@barefootjs/client 0.20.0 → 0.21.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.
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Client-side runtime for the catalogued `Date` lowering (#2274, #2292).
3
+ *
4
+ * The compiler lowers a Date-typed prop's zero-arg accessor call
5
+ * (`createdAt.toISOString()`, matched by `datePlugin` /
6
+ * `DATE_METHODS` in `packages/jsx/src/date-lowering.ts`) to
7
+ * `date(recv, "op")` on every backend. Each backend's `date` runtime
8
+ * helper must agree byte-for-byte on the dispatched value — the shared
9
+ * oracle is the `date` reference function in
10
+ * `packages/adapter-tests/vectors/cases.ts`.
11
+ *
12
+ * The CLIENT receiver shape differs from the SSR/oracle one: props are
13
+ * JSON round-tripped with no type-aware revival
14
+ * (`packages/client/src/runtime/hydrate.ts` `parseProps`), so a
15
+ * `Date`-typed prop arrives at hydration as its `toJSON()` ISO string,
16
+ * not a real `Date` instance. `recv` is therefore either:
17
+ * - a real `Date` (e.g. a value constructed client-side), or
18
+ * - an ISO-8601 string (the hydrated-prop case).
19
+ * (No `{$date: ...}` envelope — that shape is test/oracle-only, never a
20
+ * runtime value.) Both are coerced via `new Date(recv)`; a nil or
21
+ * unparseable receiver degrades to the same zero value every backend
22
+ * documents ('' for toISOString, 0 otherwise) instead of throwing.
23
+ */
24
+ export declare function date(recv: unknown, op: string): string | number;
25
+ //# sourceMappingURL=date.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"date.d.ts","sourceRoot":"","sources":["../../src/runtime/date.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAM/D"}
@@ -17,6 +17,7 @@ export { applyRestAttrs } from './apply-rest-attrs.ts';
17
17
  export { spreadAttrs } from './spread-attrs.ts';
18
18
  export { styleToCss } from './style.ts';
19
19
  export { findScope, find, $, $c, $t, qsa, qsaChildScope, qsaChildScopes, cssEscape, tAfter } from './query.ts';
20
+ export { date } from './date.ts';
20
21
  export { hydrate, rehydrateAll, rehydrateScope, disposeScope, flushHydration, getRegisteredDef } from './hydrate.ts';
21
22
  export { registerComponent, getComponentInit, initChild, upsertChild } from './registry.ts';
22
23
  export { insert, type BranchConfig, type BranchTemplateResult } from './insert.ts';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"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"}
@@ -174,6 +174,7 @@ var BF_PORTAL_ID = "bf-pi";
174
174
  var BF_PORTAL_PLACEHOLDER = "bf-pp";
175
175
  var BF_PARENT_OWNED_PREFIX = "^";
176
176
  var BF_SCOPE_COMMENT_PREFIX = "bf-scope:";
177
+ var BF_SCOPE_COMMENT_END_PREFIX = "bf-/scope:";
177
178
  var BF_LOOP_START = "bf-loop";
178
179
  var BF_LOOP_END = "bf-/loop";
179
180
  var BF_LOOP_ITEM = "bf-loop-i";
@@ -494,7 +495,15 @@ function getPortalScopeId(element) {
494
495
  return info?.scopeId ?? null;
495
496
  }
496
497
  function getCommentScopeBoundary(commentNode) {
497
- const isLoopItem = commentNode.nodeValue?.startsWith(`${BF_LOOP_ITEM}:`) ?? false;
498
+ const anchorValue = commentNode.nodeValue ?? "";
499
+ const isLoopItem = anchorValue.startsWith(`${BF_LOOP_ITEM}:`);
500
+ let endValue = null;
501
+ if (!isLoopItem && anchorValue.startsWith(BF_SCOPE_COMMENT_PREFIX)) {
502
+ const rest = anchorValue.slice(BF_SCOPE_COMMENT_PREFIX.length);
503
+ const pipeIdx = rest.indexOf("|");
504
+ endValue = BF_SCOPE_COMMENT_END_PREFIX + (pipeIdx >= 0 ? rest.slice(0, pipeIdx) : rest);
505
+ }
506
+ let legacyBoundary = null;
498
507
  let node = commentNode.nextSibling;
499
508
  while (node) {
500
509
  if (node.nodeType === Node.COMMENT_NODE) {
@@ -503,13 +512,18 @@ function getCommentScopeBoundary(commentNode) {
503
512
  if (value.startsWith(`${BF_LOOP_ITEM}:`) || value.startsWith(`${BF_LOOP_END}:`)) {
504
513
  return node;
505
514
  }
506
- } else if (value.startsWith(BF_SCOPE_COMMENT_PREFIX)) {
515
+ } else if (endValue && value === endValue) {
507
516
  return node;
517
+ } else if (value.startsWith(BF_SCOPE_COMMENT_PREFIX)) {
518
+ if (!endValue)
519
+ return node;
520
+ if (!legacyBoundary)
521
+ legacyBoundary = node;
508
522
  }
509
523
  }
510
524
  node = node.nextSibling;
511
525
  }
512
- return null;
526
+ return legacyBoundary;
513
527
  }
514
528
 
515
529
  // src/runtime/query.ts
@@ -626,7 +640,41 @@ function belongsToScope(element, scope) {
626
640
  if (element.getAttribute(BF_SCOPE))
627
641
  return false;
628
642
  const nearestScope = element.closest(`[${BF_SCOPE}]`);
629
- return nearestScope === scope;
643
+ if (nearestScope !== scope)
644
+ return false;
645
+ return !isInsideNestedCommentScope(element, scope);
646
+ }
647
+ function isInsideNestedCommentScope(element, scope) {
648
+ let current = element;
649
+ while (current !== scope) {
650
+ const parent = current.parentNode;
651
+ if (!parent)
652
+ return false;
653
+ if (isTopLevelCommentScopeNode(current))
654
+ return true;
655
+ current = parent;
656
+ }
657
+ return false;
658
+ }
659
+ function isTopLevelCommentScopeNode(node) {
660
+ const parent = node.parentNode;
661
+ if (!parent)
662
+ return false;
663
+ let depth = 0;
664
+ let sib = parent.firstChild;
665
+ while (sib) {
666
+ if (sib === node)
667
+ return depth > 0;
668
+ if (sib.nodeType === Node.COMMENT_NODE) {
669
+ const value = sib.nodeValue ?? "";
670
+ if (value.startsWith(BF_SCOPE_COMMENT_PREFIX))
671
+ depth++;
672
+ else if (value.startsWith(BF_SCOPE_COMMENT_END_PREFIX) && depth > 0)
673
+ depth--;
674
+ }
675
+ sib = sib.nextSibling;
676
+ }
677
+ return false;
630
678
  }
631
679
  function isInCommentScopeRange(element, commentNode) {
632
680
  const boundary = getCommentScopeBoundary(commentNode);
@@ -651,7 +699,8 @@ function find(scope, selector, ignoreScope) {
651
699
  if (commentInfo) {
652
700
  if (candidate.parentElement === commentInfo.commentNode.parentElement)
653
701
  return candidate;
654
- if (!candidate.closest(`[${BF_SCOPE}]`))
702
+ const nearestScope = candidate.closest(`[${BF_SCOPE}]`);
703
+ if (!nearestScope || !isInCommentScopeRange(nearestScope, commentInfo.commentNode))
655
704
  return candidate;
656
705
  } else {
657
706
  if (belongsToScope(candidate, scope))
@@ -766,9 +815,48 @@ function $cSingle(scope, id) {
766
815
  }
767
816
  if (scope.matches?.(`[${BF_SCOPE}$="_${cleanId}"]`))
768
817
  return scope;
769
- return null;
818
+ return findCommentChildScope(scope, parentScopeIds, cleanId);
819
+ }
820
+ return findChildScope(scope, `[${BF_SCOPE}$="_${cleanId}"]`) ?? findCommentChildScope(scope, [], cleanId);
821
+ }
822
+ function findCommentChildScope(scope, parentIds, slotId) {
823
+ for (const comment of commentsInScope(scope)) {
824
+ const id = parseCommentScopeId(comment.nodeValue ?? "", BF_SCOPE_COMMENT_PREFIX);
825
+ if (!id)
826
+ continue;
827
+ const matches = parentIds.length > 0 ? parentIds.some((parentId) => id === `${parentId}_${slotId}`) : id.endsWith(`_${slotId}`) && !NESTED_SLOT_SUFFIX.test(id);
828
+ if (!matches)
829
+ continue;
830
+ const proxyEl = nextElementSibling(comment) ?? comment.parentElement;
831
+ if (!proxyEl)
832
+ return null;
833
+ commentScopeRegistry.set(proxyEl, { commentNode: comment, scopeId: id });
834
+ return proxyEl;
835
+ }
836
+ return null;
837
+ }
838
+ function* commentsInScope(scope) {
839
+ const commentInfo = commentScopeRegistry.get(scope);
840
+ if (commentInfo) {
841
+ const boundary = getCommentScopeBoundary(commentInfo.commentNode);
842
+ let node = commentInfo.commentNode.nextSibling;
843
+ while (node && node !== boundary) {
844
+ if (node.nodeType === Node.COMMENT_NODE) {
845
+ yield node;
846
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
847
+ yield* commentsUnder(node);
848
+ }
849
+ node = node.nextSibling;
850
+ }
851
+ return;
852
+ }
853
+ yield* commentsUnder(scope);
854
+ }
855
+ function* commentsUnder(root) {
856
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_COMMENT);
857
+ while (walker.nextNode()) {
858
+ yield walker.currentNode;
770
859
  }
771
- return findChildScope(scope, `[${BF_SCOPE}$="_${cleanId}"]`);
772
860
  }
773
861
  function findChildScope(scope, selector) {
774
862
  if (scope.matches?.(selector))
@@ -868,7 +956,10 @@ function findSsrScopeBySlotIn(parent, slotId, anchorScope, selfMatch) {
868
956
  const suffixSelector = `[${BF_SCOPE}$="_${slotId}"]`;
869
957
  if (selfMatch && parent.matches(suffixSelector))
870
958
  return parent;
871
- return parent.querySelector(suffixSelector);
959
+ const bySuffix = parent.querySelector(suffixSelector);
960
+ if (bySuffix)
961
+ return bySuffix;
962
+ return findCommentChildScope(parent, parentBfs ? [parentBfs] : [], slotId);
872
963
  }
873
964
 
874
965
  // src/runtime/registry.ts
@@ -2280,6 +2371,16 @@ function spreadAttrs(obj) {
2280
2371
  }
2281
2372
  return parts.join(" ");
2282
2373
  }
2374
+ // src/runtime/date.ts
2375
+ function date(recv, op) {
2376
+ const zero = op === "toISOString" ? "" : 0;
2377
+ if (recv === null || recv === undefined)
2378
+ return zero;
2379
+ const d = recv instanceof Date ? recv : new Date(recv);
2380
+ if (Number.isNaN(d.getTime()))
2381
+ return zero;
2382
+ return d[op]();
2383
+ }
2283
2384
  // src/runtime/insert.ts
2284
2385
  import { createEffect as createEffect3, untrack as untrack2 } from "@barefootjs/client/reactive";
2285
2386
  function makeRegion(scope) {
@@ -2676,6 +2777,7 @@ function render(container, nameOrDef, props = {}) {
2676
2777
  for (const node of Array.from(tpl.content.childNodes)) {
2677
2778
  container.appendChild(node);
2678
2779
  }
2780
+ container.appendChild(document.createComment(`${BF_SCOPE_COMMENT_END_PREFIX}${scopeId}`));
2679
2781
  const proxyEl = rootElements[0];
2680
2782
  commentScopeRegistry.set(proxyEl, { commentNode, scopeId });
2681
2783
  init(proxyEl, props);
@@ -2756,6 +2858,7 @@ export {
2756
2858
  escapeAttr,
2757
2859
  endTurn,
2758
2860
  disposeScope,
2861
+ date,
2759
2862
  cssEscape,
2760
2863
  createSignal2 as createSignal,
2761
2864
  createSelector,
@@ -74,6 +74,30 @@ export declare function $(scope: Element | null, ...ids: string[]): (Element | n
74
74
  * Always returns an array — callers use destructuring.
75
75
  */
76
76
  export declare function $c(scope: Element | null, ...ids: string[]): (Element | null)[];
77
+ /**
78
+ * Resolve a comment-anchored child scope (fragment-root child, #2289).
79
+ *
80
+ * A child component whose root is a JSX Fragment has no element carrying
81
+ * `bf-s`/`bf-h`/`bf-m` — its scope is declared by a
82
+ * `<!--bf-scope:<parentId>_<slotId>|h=...|m=...-->` comment. The element
83
+ * selectors in `$cSingle` / `findSsrScopeBySlotIn` can never match it, so
84
+ * without this fallback the parent's `initChild(...)` receives `null` and
85
+ * the child is never initialized: every callback prop dies silently and
86
+ * no reactive update ever reaches the child.
87
+ *
88
+ * Matches on the scope id embedded in the comment (`<parentId>_<slotId>`,
89
+ * the same convention the element path's `[bf-s$="<parentId>_<slotId>"]`
90
+ * selector relies on) rather than on the `|h=`/`|m=` segments — the scope
91
+ * id is the first `|`-free token, so matching it cannot be confused by
92
+ * props JSON that happens to contain `|h=`.
93
+ *
94
+ * On match, the proxy element (first element sibling after the comment,
95
+ * falling back to the comment's parent) is registered in
96
+ * `commentScopeRegistry` so the child's own `$`/`$t` queries walk the
97
+ * comment range — the same wiring `hydrateCommentScope` performs for
98
+ * walker-owned root fragments.
99
+ */
100
+ export declare function findCommentChildScope(scope: Element, parentIds: readonly string[], slotId: string): Element | null;
77
101
  /**
78
102
  * Find Text nodes for reactive text expressions marked by comment nodes.
79
103
  * Expects marker format: <!--bf:sX-->text<!--/-->
@@ -1 +1 @@
1
- {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../src/runtime/query.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,sFAAsF;AACtF,eAAO,MAAM,SAAS,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAGJ,CAAA;AAqCnC;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,OAAO,GAAG,QAAQ,GAAG,IAAI,EACjC,OAAO,CAAC,EAAE,OAAO,GAChB,OAAO,GAAG,IAAI,CAoDhB;AA6HD;;;;;;;;;;GAUG;AACH,wBAAgB,IAAI,CAClB,KAAK,EAAE,OAAO,GAAG,IAAI,EACrB,QAAQ,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,OAAO,GACpB,OAAO,GAAG,IAAI,CA2BhB;AAuBD;;;;;;;GAOG;AACH,wBAAgB,GAAG,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CA0BxE;AA+CD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAU9E;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,EAAE,CAO1E;AAED;;;;;;;;GAQG;AACH,wBAAgB,CAAC,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,EAAE,GAAG,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,OAAO,GAAG,IAAI,CAAC,EAAE,CAO7E;AAED;;;;;GAKG;AACH,wBAAgB,EAAE,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,EAAE,GAAG,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,OAAO,GAAG,IAAI,CAAC,EAAE,CAE9E;AA+FD;;;;;;;GAOG;AACH,wBAAgB,EAAE,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,EAAE,GAAG,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,EAAE,CAkC3E;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAS3D;AAED,OAAO,EAAE,oBAAoB,IAAI,MAAM,EAAE,CAAA"}
1
+ {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../src/runtime/query.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,sFAAsF;AACtF,eAAO,MAAM,SAAS,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAGJ,CAAA;AAqCnC;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,OAAO,GAAG,QAAQ,GAAG,IAAI,EACjC,OAAO,CAAC,EAAE,OAAO,GAChB,OAAO,GAAG,IAAI,CAoDhB;AAiLD;;;;;;;;;;GAUG;AACH,wBAAgB,IAAI,CAClB,KAAK,EAAE,OAAO,GAAG,IAAI,EACrB,QAAQ,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,OAAO,GACpB,OAAO,GAAG,IAAI,CAiChB;AAuBD;;;;;;;GAOG;AACH,wBAAgB,GAAG,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CA0BxE;AA+CD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAU9E;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,EAAE,CAO1E;AAED;;;;;;;;GAQG;AACH,wBAAgB,CAAC,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,EAAE,GAAG,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,OAAO,GAAG,IAAI,CAAC,EAAE,CAO7E;AAED;;;;;GAKG;AACH,wBAAgB,EAAE,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,EAAE,GAAG,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,OAAO,GAAG,IAAI,CAAC,EAAE,CAE9E;AAkDD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,OAAO,EACd,SAAS,EAAE,SAAS,MAAM,EAAE,EAC5B,MAAM,EAAE,MAAM,GACb,OAAO,GAAG,IAAI,CAehB;AAmFD;;;;;;;GAOG;AACH,wBAAgB,EAAE,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,EAAE,GAAG,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,EAAE,CAkC3E;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAS3D;AAED,OAAO,EAAE,oBAAoB,IAAI,MAAM,EAAE,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/runtime/render.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAQH,OAAO,KAAK,EAAE,YAAY,EAAU,MAAM,YAAY,CAAA;AAEtD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,MAAM,CACpB,SAAS,EAAE,WAAW,EACtB,SAAS,EAAE,MAAM,GAAG,YAAY,EAChC,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAClC,IAAI,CAiFN"}
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/runtime/render.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAQH,OAAO,KAAK,EAAE,YAAY,EAAU,MAAM,YAAY,CAAA;AAEtD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,MAAM,CACpB,SAAS,EAAE,WAAW,EACtB,SAAS,EAAE,MAAM,GAAG,YAAY,EAChC,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAClC,IAAI,CAoFN"}
@@ -24,8 +24,12 @@ export declare function getPortalScopeId(element: Element): string | null;
24
24
  * Find the end boundary for a comment-based scope.
25
25
  *
26
26
  * The boundary depends on the anchor's kind:
27
- * - `bf-scope:` anchor (fragment-root component): boundary is the next
28
- * `bf-scope:` comment or the end of the parent's children (unchanged).
27
+ * - `bf-scope:` anchor (fragment-root component): boundary is this scope's
28
+ * own `bf-/scope:<scopeId>` end marker when present (#2289). Without one
29
+ * (SSR HTML from an adapter runtime predating the end marker), fall back
30
+ * to the next `bf-scope:` comment or the end of the parent's children —
31
+ * the historical heuristic, which over-extends the range past the
32
+ * fragment's real last root.
29
33
  * - `bf-loop-i:<key>` anchor (loop item, #1665): boundary is the next
30
34
  * loop-item anchor (`bf-loop-i:*`) or the loop end marker (`bf-/loop:*`),
31
35
  * so one item's range never bleeds into the next item or past the loop.
@@ -1 +1 @@
1
- {"version":3,"file":"scope.d.ts","sourceRoot":"","sources":["../../src/runtime/scope.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,OAAO,CAAA;IACpB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;GAEG;AACH,eAAO,MAAM,oBAAoB,oCAA2C,CAAA;AAE5E;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAGhE;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,OAAO,GAAG,IAAI,GAAG,IAAI,CAiBzE"}
1
+ {"version":3,"file":"scope.d.ts","sourceRoot":"","sources":["../../src/runtime/scope.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,OAAO,CAAA;IACpB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;GAEG;AACH,eAAO,MAAM,oBAAoB,oCAA2C,CAAA;AAE5E;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAGhE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,OAAO,GAAG,IAAI,GAAG,IAAI,CAqCzE"}
@@ -1 +1 @@
1
- {"version":3,"file":"slot-resolver.d.ts","sourceRoot":"","sources":["../../src/runtime/slot-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH;;gEAEgE;AAChE,wBAAgB,aAAa,CAAC,MAAM,EAAE,OAAO,EAAE,WAAW,CAAC,EAAE,OAAO,GAAG,IAAI,GAAG,MAAM,CAInF;AAED;;;;6BAI6B;AAC7B,wBAAgB,aAAa,CAC3B,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,MAAM,EACd,WAAW,CAAC,EAAE,OAAO,GAAG,IAAI,GAC3B;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAEnC;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,OAAO,GAAG,IAAI,GAAG,SAAS,EACvC,SAAS,EAAE,OAAO,GACjB,WAAW,GAAG,IAAI,CAapB"}
1
+ {"version":3,"file":"slot-resolver.d.ts","sourceRoot":"","sources":["../../src/runtime/slot-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH;;gEAEgE;AAChE,wBAAgB,aAAa,CAAC,MAAM,EAAE,OAAO,EAAE,WAAW,CAAC,EAAE,OAAO,GAAG,IAAI,GAAG,MAAM,CAInF;AAED;;;;6BAI6B;AAC7B,wBAAgB,aAAa,CAC3B,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,MAAM,EACd,WAAW,CAAC,EAAE,OAAO,GAAG,IAAI,GAC3B;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAEnC;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,OAAO,GAAG,IAAI,GAAG,SAAS,EACvC,SAAS,EAAE,OAAO,GACjB,WAAW,GAAG,IAAI,CAsBpB"}
@@ -10,6 +10,7 @@ var BF_PORTAL_ID = "bf-pi";
10
10
  var BF_PORTAL_PLACEHOLDER = "bf-pp";
11
11
  var BF_PARENT_OWNED_PREFIX = "^";
12
12
  var BF_SCOPE_COMMENT_PREFIX = "bf-scope:";
13
+ var BF_SCOPE_COMMENT_END_PREFIX = "bf-/scope:";
13
14
  var BF_LOOP_START = "bf-loop";
14
15
  var BF_LOOP_END = "bf-/loop";
15
16
  var BF_LOOP_ITEM = "bf-loop-i";
@@ -902,7 +903,15 @@ function getPortalScopeId(element) {
902
903
  return info?.scopeId ?? null;
903
904
  }
904
905
  function getCommentScopeBoundary(commentNode) {
905
- const isLoopItem = commentNode.nodeValue?.startsWith(`${BF_LOOP_ITEM}:`) ?? false;
906
+ const anchorValue = commentNode.nodeValue ?? "";
907
+ const isLoopItem = anchorValue.startsWith(`${BF_LOOP_ITEM}:`);
908
+ let endValue = null;
909
+ if (!isLoopItem && anchorValue.startsWith(BF_SCOPE_COMMENT_PREFIX)) {
910
+ const rest = anchorValue.slice(BF_SCOPE_COMMENT_PREFIX.length);
911
+ const pipeIdx = rest.indexOf("|");
912
+ endValue = BF_SCOPE_COMMENT_END_PREFIX + (pipeIdx >= 0 ? rest.slice(0, pipeIdx) : rest);
913
+ }
914
+ let legacyBoundary = null;
906
915
  let node = commentNode.nextSibling;
907
916
  while (node) {
908
917
  if (node.nodeType === Node.COMMENT_NODE) {
@@ -911,13 +920,18 @@ function getCommentScopeBoundary(commentNode) {
911
920
  if (value.startsWith(`${BF_LOOP_ITEM}:`) || value.startsWith(`${BF_LOOP_END}:`)) {
912
921
  return node;
913
922
  }
914
- } else if (value.startsWith(BF_SCOPE_COMMENT_PREFIX)) {
923
+ } else if (endValue && value === endValue) {
915
924
  return node;
925
+ } else if (value.startsWith(BF_SCOPE_COMMENT_PREFIX)) {
926
+ if (!endValue)
927
+ return node;
928
+ if (!legacyBoundary)
929
+ legacyBoundary = node;
916
930
  }
917
931
  }
918
932
  node = node.nextSibling;
919
933
  }
920
- return null;
934
+ return legacyBoundary;
921
935
  }
922
936
 
923
937
  // src/runtime/query.ts
@@ -1034,7 +1048,41 @@ function belongsToScope(element, scope) {
1034
1048
  if (element.getAttribute(BF_SCOPE))
1035
1049
  return false;
1036
1050
  const nearestScope = element.closest(`[${BF_SCOPE}]`);
1037
- return nearestScope === scope;
1051
+ if (nearestScope !== scope)
1052
+ return false;
1053
+ return !isInsideNestedCommentScope(element, scope);
1054
+ }
1055
+ function isInsideNestedCommentScope(element, scope) {
1056
+ let current = element;
1057
+ while (current !== scope) {
1058
+ const parent = current.parentNode;
1059
+ if (!parent)
1060
+ return false;
1061
+ if (isTopLevelCommentScopeNode(current))
1062
+ return true;
1063
+ current = parent;
1064
+ }
1065
+ return false;
1066
+ }
1067
+ function isTopLevelCommentScopeNode(node) {
1068
+ const parent = node.parentNode;
1069
+ if (!parent)
1070
+ return false;
1071
+ let depth = 0;
1072
+ let sib = parent.firstChild;
1073
+ while (sib) {
1074
+ if (sib === node)
1075
+ return depth > 0;
1076
+ if (sib.nodeType === Node.COMMENT_NODE) {
1077
+ const value = sib.nodeValue ?? "";
1078
+ if (value.startsWith(BF_SCOPE_COMMENT_PREFIX))
1079
+ depth++;
1080
+ else if (value.startsWith(BF_SCOPE_COMMENT_END_PREFIX) && depth > 0)
1081
+ depth--;
1082
+ }
1083
+ sib = sib.nextSibling;
1084
+ }
1085
+ return false;
1038
1086
  }
1039
1087
  function isInCommentScopeRange(element, commentNode) {
1040
1088
  const boundary = getCommentScopeBoundary(commentNode);
@@ -1059,7 +1107,8 @@ function find(scope, selector, ignoreScope) {
1059
1107
  if (commentInfo) {
1060
1108
  if (candidate.parentElement === commentInfo.commentNode.parentElement)
1061
1109
  return candidate;
1062
- if (!candidate.closest(`[${BF_SCOPE}]`))
1110
+ const nearestScope = candidate.closest(`[${BF_SCOPE}]`);
1111
+ if (!nearestScope || !isInCommentScopeRange(nearestScope, commentInfo.commentNode))
1063
1112
  return candidate;
1064
1113
  } else {
1065
1114
  if (belongsToScope(candidate, scope))
@@ -1174,9 +1223,48 @@ function $cSingle(scope, id) {
1174
1223
  }
1175
1224
  if (scope.matches?.(`[${BF_SCOPE}$="_${cleanId}"]`))
1176
1225
  return scope;
1177
- return null;
1226
+ return findCommentChildScope(scope, parentScopeIds, cleanId);
1227
+ }
1228
+ return findChildScope(scope, `[${BF_SCOPE}$="_${cleanId}"]`) ?? findCommentChildScope(scope, [], cleanId);
1229
+ }
1230
+ function findCommentChildScope(scope, parentIds, slotId) {
1231
+ for (const comment of commentsInScope(scope)) {
1232
+ const id = parseCommentScopeId(comment.nodeValue ?? "", BF_SCOPE_COMMENT_PREFIX);
1233
+ if (!id)
1234
+ continue;
1235
+ const matches = parentIds.length > 0 ? parentIds.some((parentId) => id === `${parentId}_${slotId}`) : id.endsWith(`_${slotId}`) && !NESTED_SLOT_SUFFIX.test(id);
1236
+ if (!matches)
1237
+ continue;
1238
+ const proxyEl = nextElementSibling(comment) ?? comment.parentElement;
1239
+ if (!proxyEl)
1240
+ return null;
1241
+ commentScopeRegistry.set(proxyEl, { commentNode: comment, scopeId: id });
1242
+ return proxyEl;
1243
+ }
1244
+ return null;
1245
+ }
1246
+ function* commentsInScope(scope) {
1247
+ const commentInfo = commentScopeRegistry.get(scope);
1248
+ if (commentInfo) {
1249
+ const boundary = getCommentScopeBoundary(commentInfo.commentNode);
1250
+ let node = commentInfo.commentNode.nextSibling;
1251
+ while (node && node !== boundary) {
1252
+ if (node.nodeType === Node.COMMENT_NODE) {
1253
+ yield node;
1254
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
1255
+ yield* commentsUnder(node);
1256
+ }
1257
+ node = node.nextSibling;
1258
+ }
1259
+ return;
1260
+ }
1261
+ yield* commentsUnder(scope);
1262
+ }
1263
+ function* commentsUnder(root) {
1264
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_COMMENT);
1265
+ while (walker.nextNode()) {
1266
+ yield walker.currentNode;
1178
1267
  }
1179
- return findChildScope(scope, `[${BF_SCOPE}$="_${cleanId}"]`);
1180
1268
  }
1181
1269
  function findChildScope(scope, selector) {
1182
1270
  if (scope.matches?.(selector))
@@ -1276,7 +1364,10 @@ function findSsrScopeBySlotIn(parent, slotId, anchorScope, selfMatch) {
1276
1364
  const suffixSelector = `[${BF_SCOPE}$="_${slotId}"]`;
1277
1365
  if (selfMatch && parent.matches(suffixSelector))
1278
1366
  return parent;
1279
- return parent.querySelector(suffixSelector);
1367
+ const bySuffix = parent.querySelector(suffixSelector);
1368
+ if (bySuffix)
1369
+ return bySuffix;
1370
+ return findCommentChildScope(parent, parentBfs ? [parentBfs] : [], slotId);
1280
1371
  }
1281
1372
 
1282
1373
  // src/runtime/registry.ts
@@ -2682,6 +2773,16 @@ function spreadAttrs(obj) {
2682
2773
  }
2683
2774
  return parts.join(" ");
2684
2775
  }
2776
+ // src/runtime/date.ts
2777
+ function date(recv, op) {
2778
+ const zero = op === "toISOString" ? "" : 0;
2779
+ if (recv === null || recv === undefined)
2780
+ return zero;
2781
+ const d = recv instanceof Date ? recv : new Date(recv);
2782
+ if (Number.isNaN(d.getTime()))
2783
+ return zero;
2784
+ return d[op]();
2785
+ }
2685
2786
  // src/runtime/insert.ts
2686
2787
  function makeRegion(scope) {
2687
2788
  if (scope.nodeType === Node.COMMENT_NODE) {
@@ -3076,6 +3177,7 @@ function render(container, nameOrDef, props = {}) {
3076
3177
  for (const node of Array.from(tpl.content.childNodes)) {
3077
3178
  container.appendChild(node);
3078
3179
  }
3180
+ container.appendChild(document.createComment(`${BF_SCOPE_COMMENT_END_PREFIX}${scopeId}`));
3079
3181
  const proxyEl = rootElements[0];
3080
3182
  commentScopeRegistry.set(proxyEl, { commentNode, scopeId });
3081
3183
  init(proxyEl, props);
@@ -3156,6 +3258,7 @@ export {
3156
3258
  escapeAttr,
3157
3259
  endTurn,
3158
3260
  disposeScope,
3261
+ date,
3159
3262
  cssEscape,
3160
3263
  createSignal,
3161
3264
  createSelector,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/client",
3
- "version": "0.20.0",
3
+ "version": "0.21.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.20.0"
58
+ "@barefootjs/shared": "0.21.2"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "@barefootjs/jsx": ">=0.2.0"
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Client-side runtime for the catalogued `Date` lowering (#2274, #2292).
3
+ *
4
+ * The compiler lowers a Date-typed prop's zero-arg accessor call
5
+ * (`createdAt.toISOString()`, matched by `datePlugin` /
6
+ * `DATE_METHODS` in `packages/jsx/src/date-lowering.ts`) to
7
+ * `date(recv, "op")` on every backend. Each backend's `date` runtime
8
+ * helper must agree byte-for-byte on the dispatched value — the shared
9
+ * oracle is the `date` reference function in
10
+ * `packages/adapter-tests/vectors/cases.ts`.
11
+ *
12
+ * The CLIENT receiver shape differs from the SSR/oracle one: props are
13
+ * JSON round-tripped with no type-aware revival
14
+ * (`packages/client/src/runtime/hydrate.ts` `parseProps`), so a
15
+ * `Date`-typed prop arrives at hydration as its `toJSON()` ISO string,
16
+ * not a real `Date` instance. `recv` is therefore either:
17
+ * - a real `Date` (e.g. a value constructed client-side), or
18
+ * - an ISO-8601 string (the hydrated-prop case).
19
+ * (No `{$date: ...}` envelope — that shape is test/oracle-only, never a
20
+ * runtime value.) Both are coerced via `new Date(recv)`; a nil or
21
+ * unparseable receiver degrades to the same zero value every backend
22
+ * documents ('' for toISOString, 0 otherwise) instead of throwing.
23
+ */
24
+ export function date(recv: unknown, op: string): string | number {
25
+ const zero = op === 'toISOString' ? '' : 0
26
+ if (recv === null || recv === undefined) return zero
27
+ const d = recv instanceof Date ? recv : new Date(recv as string | number)
28
+ if (Number.isNaN(d.getTime())) return zero
29
+ return (d as unknown as Record<string, () => string | number>)[op]()
30
+ }
@@ -103,6 +103,7 @@ export { styleToCss } from './style.ts'
103
103
 
104
104
  // Runtime helpers
105
105
  export { findScope, find, $, $c, $t, qsa, qsaChildScope, qsaChildScopes, cssEscape, tAfter } from './query.ts'
106
+ export { date } from './date.ts'
106
107
  export { hydrate, rehydrateAll, rehydrateScope, disposeScope, flushHydration, getRegisteredDef } from './hydrate.ts'
107
108
  export { registerComponent, getComponentInit, initChild, upsertChild } from './registry.ts'
108
109
  export { insert, type BranchConfig, type BranchTemplateResult } from './insert.ts'
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { commentScopeRegistry, getCommentScopeBoundary } from './scope.ts'
10
10
  import { hydratedScopes } from './hydration-state.ts'
11
- import { BF_SCOPE, BF_SLOT, BF_PORTAL_OWNER, BF_PARENT_OWNED_PREFIX, BF_SCOPE_COMMENT_PREFIX } from '@barefootjs/shared'
11
+ import { BF_SCOPE, BF_SLOT, BF_PORTAL_OWNER, BF_PARENT_OWNED_PREFIX, BF_SCOPE_COMMENT_PREFIX, BF_SCOPE_COMMENT_END_PREFIX } from '@barefootjs/shared'
12
12
 
13
13
  /** CSS attribute-value escape with a fallback for environments lacking CSS.escape. */
14
14
  export const cssEscape: (s: string) => string =
@@ -222,7 +222,59 @@ function belongsToScope(element: Element, scope: Element): boolean {
222
222
 
223
223
  // Check if nearest scope matches
224
224
  const nearestScope = element.closest(`[${BF_SCOPE}]`)
225
- return nearestScope === scope
225
+ if (nearestScope !== scope) return false
226
+
227
+ // A fragment-rooted child mounted between `element` and `scope` has no
228
+ // `bf-s` element of its own (#2302) — `closest()` walks straight past it
229
+ // to `scope`, so a slot search on `scope` can wrongly claim a descendant
230
+ // that actually belongs to that nested child's comment scope. Reject it.
231
+ return !isInsideNestedCommentScope(element, scope)
232
+ }
233
+
234
+ /**
235
+ * True if some ancestor of `element` (strictly below `scope`) is a
236
+ * top-level node of a fragment-rooted child's `<!--bf-scope:...-->` range —
237
+ * i.e. `element` structurally belongs to a nested child component even
238
+ * though that child has no `bf-s`-attributed element for `.closest()` to
239
+ * stop at, and regardless of whether `commentScopeRegistry` has registered
240
+ * it yet (a parent's own slot lookups can run before its child-scope
241
+ * lookups register the child, #2302).
242
+ */
243
+ function isInsideNestedCommentScope(element: Element, scope: Element): boolean {
244
+ let current: Node = element
245
+ while (current !== scope) {
246
+ const parent: Node | null = current.parentNode
247
+ if (!parent) return false
248
+ if (isTopLevelCommentScopeNode(current)) return true
249
+ current = parent
250
+ }
251
+ return false
252
+ }
253
+
254
+ /**
255
+ * True if `node` falls inside a `<!--bf-scope:...-->` … `<!--bf-/scope:...-->`
256
+ * range among its own siblings — i.e. `node` is (or is inside) a top-level
257
+ * element of a fragment-rooted child mounted as `node`'s sibling. Single
258
+ * left-to-right pass over the sibling list with a nesting depth counter, so
259
+ * a nested child's own begin/end pair doesn't prematurely close an outer
260
+ * one. Missing end markers (older SSR output) leave depth un-decremented,
261
+ * matching `getCommentScopeBoundary`'s legacy end-of-parent fallback.
262
+ */
263
+ function isTopLevelCommentScopeNode(node: Node): boolean {
264
+ const parent = node.parentNode
265
+ if (!parent) return false
266
+ let depth = 0
267
+ let sib: Node | null = parent.firstChild
268
+ while (sib) {
269
+ if (sib === node) return depth > 0
270
+ if (sib.nodeType === Node.COMMENT_NODE) {
271
+ const value = (sib as Comment).nodeValue ?? ''
272
+ if (value.startsWith(BF_SCOPE_COMMENT_PREFIX)) depth++
273
+ else if (value.startsWith(BF_SCOPE_COMMENT_END_PREFIX) && depth > 0) depth--
274
+ }
275
+ sib = sib.nextSibling
276
+ }
277
+ return false
226
278
  }
227
279
 
228
280
  /**
@@ -271,10 +323,16 @@ export function find(
271
323
  if (ignoreScope) return candidate
272
324
  if (commentInfo) {
273
325
  // Comment scope: top-level siblings in the comment range are always
274
- // accepted (even if they have bf-s, like proxy elements). Descendants
275
- // are accepted only if not inside a nested bf-s scope.
326
+ // accepted (even if they have bf-s, like proxy elements). A descendant
327
+ // nested inside one of those siblings is accepted unless a *nested*
328
+ // child scope sits between it and this one — i.e. its nearest bf-s
329
+ // ancestor falls within this comment's own sibling range. An ancestor
330
+ // bf-s element outside that range doesn't disqualify the candidate:
331
+ // a fragment-root child mounted inside a normal parent island always
332
+ // has one, and `.closest()` alone can't tell the two apart (#2302).
276
333
  if (candidate.parentElement === commentInfo.commentNode.parentElement) return candidate
277
- if (!candidate.closest(`[${BF_SCOPE}]`)) return candidate
334
+ const nearestScope = candidate.closest(`[${BF_SCOPE}]`)
335
+ if (!nearestScope || !isInCommentScopeRange(nearestScope, commentInfo.commentNode)) return candidate
278
336
  } else {
279
337
  if (belongsToScope(candidate, scope)) return candidate
280
338
  }
@@ -492,11 +550,91 @@ function $cSingle(scope: Element | null, id: string): Element | null {
492
550
  // Precise match found nothing. Check if scope itself matches the short suffix
493
551
  // (fragment root / inlined component where scope IS the child).
494
552
  if (scope.matches?.(`[${BF_SCOPE}$="_${cleanId}"]`)) return scope
495
- return null
553
+ // Fragment-root child: no element carries the child's scope id — it is
554
+ // declared by a bf-scope: comment instead (#2289).
555
+ return findCommentChildScope(scope, parentScopeIds, cleanId)
496
556
  }
497
557
 
498
558
  // Fallback: no parent scope ID available — use short suffix match (best-effort)
499
559
  return findChildScope(scope, `[${BF_SCOPE}$="_${cleanId}"]`)
560
+ ?? findCommentChildScope(scope, [], cleanId)
561
+ }
562
+
563
+ /**
564
+ * Resolve a comment-anchored child scope (fragment-root child, #2289).
565
+ *
566
+ * A child component whose root is a JSX Fragment has no element carrying
567
+ * `bf-s`/`bf-h`/`bf-m` — its scope is declared by a
568
+ * `<!--bf-scope:<parentId>_<slotId>|h=...|m=...-->` comment. The element
569
+ * selectors in `$cSingle` / `findSsrScopeBySlotIn` can never match it, so
570
+ * without this fallback the parent's `initChild(...)` receives `null` and
571
+ * the child is never initialized: every callback prop dies silently and
572
+ * no reactive update ever reaches the child.
573
+ *
574
+ * Matches on the scope id embedded in the comment (`<parentId>_<slotId>`,
575
+ * the same convention the element path's `[bf-s$="<parentId>_<slotId>"]`
576
+ * selector relies on) rather than on the `|h=`/`|m=` segments — the scope
577
+ * id is the first `|`-free token, so matching it cannot be confused by
578
+ * props JSON that happens to contain `|h=`.
579
+ *
580
+ * On match, the proxy element (first element sibling after the comment,
581
+ * falling back to the comment's parent) is registered in
582
+ * `commentScopeRegistry` so the child's own `$`/`$t` queries walk the
583
+ * comment range — the same wiring `hydrateCommentScope` performs for
584
+ * walker-owned root fragments.
585
+ */
586
+ export function findCommentChildScope(
587
+ scope: Element,
588
+ parentIds: readonly string[],
589
+ slotId: string,
590
+ ): Element | null {
591
+ for (const comment of commentsInScope(scope)) {
592
+ const id = parseCommentScopeId(comment.nodeValue ?? '', BF_SCOPE_COMMENT_PREFIX)
593
+ if (!id) continue
594
+ const matches = parentIds.length > 0
595
+ ? parentIds.some(parentId => id === `${parentId}_${slotId}`)
596
+ : id.endsWith(`_${slotId}`) && !NESTED_SLOT_SUFFIX.test(id)
597
+ if (!matches) continue
598
+
599
+ const proxyEl = nextElementSibling(comment) ?? comment.parentElement
600
+ if (!proxyEl) return null
601
+ commentScopeRegistry.set(proxyEl, { commentNode: comment, scopeId: id })
602
+ return proxyEl
603
+ }
604
+ return null
605
+ }
606
+
607
+ /**
608
+ * Enumerate comment nodes in a scope's DOM range: the comment-scope
609
+ * sibling range for comment-anchored scopes, the element subtree
610
+ * otherwise. Mirrors candidatesInScope's notion of "where the scope's
611
+ * content lives", but yields comments instead of elements.
612
+ */
613
+ function* commentsInScope(scope: Element): Generator<Comment> {
614
+ const commentInfo = commentScopeRegistry.get(scope)
615
+
616
+ if (commentInfo) {
617
+ const boundary = getCommentScopeBoundary(commentInfo.commentNode)
618
+ let node: Node | null = commentInfo.commentNode.nextSibling
619
+ while (node && node !== boundary) {
620
+ if (node.nodeType === Node.COMMENT_NODE) {
621
+ yield node as Comment
622
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
623
+ yield* commentsUnder(node as Element)
624
+ }
625
+ node = node.nextSibling
626
+ }
627
+ return
628
+ }
629
+
630
+ yield* commentsUnder(scope)
631
+ }
632
+
633
+ function* commentsUnder(root: Element): Generator<Comment> {
634
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_COMMENT)
635
+ while (walker.nextNode()) {
636
+ yield walker.currentNode as Comment
637
+ }
500
638
  }
501
639
 
502
640
  /**
@@ -6,7 +6,7 @@
6
6
  * never import this module.
7
7
  */
8
8
 
9
- import { BF_SCOPE, BF_SCOPE_COMMENT_PREFIX } from '@barefootjs/shared'
9
+ import { BF_SCOPE, BF_SCOPE_COMMENT_PREFIX, BF_SCOPE_COMMENT_END_PREFIX } from '@barefootjs/shared'
10
10
  import { setParentScopeId } from './component.ts'
11
11
  import { hydratedScopes } from './hydration-state.ts'
12
12
  import { getComponentInit } from './registry.ts'
@@ -119,6 +119,9 @@ export function render(
119
119
  for (const node of Array.from(tpl.content.childNodes)) {
120
120
  container.appendChild(node)
121
121
  }
122
+ // Close the range so scope queries never leak past the fragment's last
123
+ // root (mirrors the SSR end marker, #2289).
124
+ container.appendChild(document.createComment(`${BF_SCOPE_COMMENT_END_PREFIX}${scopeId}`))
122
125
 
123
126
  const proxyEl = rootElements[0]
124
127
  commentScopeRegistry.set(proxyEl, { commentNode, scopeId })
@@ -5,7 +5,7 @@
5
5
  * Maps an element to its comment node and the sibling range boundary.
6
6
  */
7
7
 
8
- import { BF_SCOPE_COMMENT_PREFIX, BF_LOOP_ITEM, BF_LOOP_END } from '@barefootjs/shared'
8
+ import { BF_SCOPE_COMMENT_PREFIX, BF_SCOPE_COMMENT_END_PREFIX, BF_LOOP_ITEM, BF_LOOP_END } from '@barefootjs/shared'
9
9
 
10
10
  /**
11
11
  * Information about a comment-based scope.
@@ -33,14 +33,31 @@ export function getPortalScopeId(element: Element): string | null {
33
33
  * Find the end boundary for a comment-based scope.
34
34
  *
35
35
  * The boundary depends on the anchor's kind:
36
- * - `bf-scope:` anchor (fragment-root component): boundary is the next
37
- * `bf-scope:` comment or the end of the parent's children (unchanged).
36
+ * - `bf-scope:` anchor (fragment-root component): boundary is this scope's
37
+ * own `bf-/scope:<scopeId>` end marker when present (#2289). Without one
38
+ * (SSR HTML from an adapter runtime predating the end marker), fall back
39
+ * to the next `bf-scope:` comment or the end of the parent's children —
40
+ * the historical heuristic, which over-extends the range past the
41
+ * fragment's real last root.
38
42
  * - `bf-loop-i:<key>` anchor (loop item, #1665): boundary is the next
39
43
  * loop-item anchor (`bf-loop-i:*`) or the loop end marker (`bf-/loop:*`),
40
44
  * so one item's range never bleeds into the next item or past the loop.
41
45
  */
42
46
  export function getCommentScopeBoundary(commentNode: Comment): Node | null {
43
- const isLoopItem = commentNode.nodeValue?.startsWith(`${BF_LOOP_ITEM}:`) ?? false
47
+ const anchorValue = commentNode.nodeValue ?? ''
48
+ const isLoopItem = anchorValue.startsWith(`${BF_LOOP_ITEM}:`)
49
+
50
+ // Exact end-marker value for a scope anchor: `bf-/scope:<scopeId>`. The
51
+ // scope id is the `|`-free head of the anchor value, so a nested child
52
+ // scope's markers (different id) never terminate this range.
53
+ let endValue: string | null = null
54
+ if (!isLoopItem && anchorValue.startsWith(BF_SCOPE_COMMENT_PREFIX)) {
55
+ const rest = anchorValue.slice(BF_SCOPE_COMMENT_PREFIX.length)
56
+ const pipeIdx = rest.indexOf('|')
57
+ endValue = BF_SCOPE_COMMENT_END_PREFIX + (pipeIdx >= 0 ? rest.slice(0, pipeIdx) : rest)
58
+ }
59
+
60
+ let legacyBoundary: Node | null = null
44
61
  let node: Node | null = commentNode.nextSibling
45
62
  while (node) {
46
63
  if (node.nodeType === Node.COMMENT_NODE) {
@@ -49,11 +66,18 @@ export function getCommentScopeBoundary(commentNode: Comment): Node | null {
49
66
  if (value.startsWith(`${BF_LOOP_ITEM}:`) || value.startsWith(`${BF_LOOP_END}:`)) {
50
67
  return node
51
68
  }
52
- } else if (value.startsWith(BF_SCOPE_COMMENT_PREFIX)) {
69
+ } else if (endValue && value === endValue) {
53
70
  return node
71
+ } else if (value.startsWith(BF_SCOPE_COMMENT_PREFIX)) {
72
+ // Historically the range ended at the next scope comment. When this
73
+ // anchor can have an end marker, keep scanning: the comment seen
74
+ // here may belong to a nested child scope inside the range, not to
75
+ // a sibling that terminates it.
76
+ if (!endValue) return node
77
+ if (!legacyBoundary) legacyBoundary = node
54
78
  }
55
79
  }
56
80
  node = node.nextSibling
57
81
  }
58
- return null // End of parent's children
82
+ return legacyBoundary // null → end of parent's children
59
83
  }
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import { BF_SCOPE, BF_HOST, BF_AT } from '@barefootjs/shared'
14
- import { cssEscape } from './query.ts'
14
+ import { cssEscape, findCommentChildScope } from './query.ts'
15
15
 
16
16
  /** Resolve the host scope id for a slot lookup. Prefers the explicit
17
17
  * `anchorScope` because the immediate `parent` element may be a freshly-
@@ -62,5 +62,14 @@ export function findSsrScopeBySlotIn(
62
62
 
63
63
  const suffixSelector = `[${BF_SCOPE}$="_${slotId}"]`
64
64
  if (selfMatch && parent.matches(suffixSelector)) return parent as HTMLElement
65
- return parent.querySelector(suffixSelector) as HTMLElement | null
65
+ const bySuffix = parent.querySelector(suffixSelector) as HTMLElement | null
66
+ if (bySuffix) return bySuffix
67
+
68
+ // Fragment-root child: its scope is a bf-scope: comment, not an element
69
+ // carrying (bf-h, bf-m) — resolve the comment's proxy element (#2289).
70
+ return findCommentChildScope(
71
+ parent,
72
+ parentBfs ? [parentBfs] : [],
73
+ slotId,
74
+ ) as HTMLElement | null
66
75
  }