@barefootjs/client 0.19.1 → 0.21.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.
@@ -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
@@ -766,9 +780,48 @@ function $cSingle(scope, id) {
766
780
  }
767
781
  if (scope.matches?.(`[${BF_SCOPE}$="_${cleanId}"]`))
768
782
  return scope;
769
- return null;
783
+ return findCommentChildScope(scope, parentScopeIds, cleanId);
784
+ }
785
+ return findChildScope(scope, `[${BF_SCOPE}$="_${cleanId}"]`) ?? findCommentChildScope(scope, [], cleanId);
786
+ }
787
+ function findCommentChildScope(scope, parentIds, slotId) {
788
+ for (const comment of commentsInScope(scope)) {
789
+ const id = parseCommentScopeId(comment.nodeValue ?? "", BF_SCOPE_COMMENT_PREFIX);
790
+ if (!id)
791
+ continue;
792
+ const matches = parentIds.length > 0 ? parentIds.some((parentId) => id === `${parentId}_${slotId}`) : id.endsWith(`_${slotId}`) && !NESTED_SLOT_SUFFIX.test(id);
793
+ if (!matches)
794
+ continue;
795
+ const proxyEl = nextElementSibling(comment) ?? comment.parentElement;
796
+ if (!proxyEl)
797
+ return null;
798
+ commentScopeRegistry.set(proxyEl, { commentNode: comment, scopeId: id });
799
+ return proxyEl;
800
+ }
801
+ return null;
802
+ }
803
+ function* commentsInScope(scope) {
804
+ const commentInfo = commentScopeRegistry.get(scope);
805
+ if (commentInfo) {
806
+ const boundary = getCommentScopeBoundary(commentInfo.commentNode);
807
+ let node = commentInfo.commentNode.nextSibling;
808
+ while (node && node !== boundary) {
809
+ if (node.nodeType === Node.COMMENT_NODE) {
810
+ yield node;
811
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
812
+ yield* commentsUnder(node);
813
+ }
814
+ node = node.nextSibling;
815
+ }
816
+ return;
817
+ }
818
+ yield* commentsUnder(scope);
819
+ }
820
+ function* commentsUnder(root) {
821
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_COMMENT);
822
+ while (walker.nextNode()) {
823
+ yield walker.currentNode;
770
824
  }
771
- return findChildScope(scope, `[${BF_SCOPE}$="_${cleanId}"]`);
772
825
  }
773
826
  function findChildScope(scope, selector) {
774
827
  if (scope.matches?.(selector))
@@ -868,7 +921,10 @@ function findSsrScopeBySlotIn(parent, slotId, anchorScope, selfMatch) {
868
921
  const suffixSelector = `[${BF_SCOPE}$="_${slotId}"]`;
869
922
  if (selfMatch && parent.matches(suffixSelector))
870
923
  return parent;
871
- return parent.querySelector(suffixSelector);
924
+ const bySuffix = parent.querySelector(suffixSelector);
925
+ if (bySuffix)
926
+ return bySuffix;
927
+ return findCommentChildScope(parent, parentBfs ? [parentBfs] : [], slotId);
872
928
  }
873
929
 
874
930
  // src/runtime/registry.ts
@@ -2280,6 +2336,16 @@ function spreadAttrs(obj) {
2280
2336
  }
2281
2337
  return parts.join(" ");
2282
2338
  }
2339
+ // src/runtime/date.ts
2340
+ function date(recv, op) {
2341
+ const zero = op === "toISOString" ? "" : 0;
2342
+ if (recv === null || recv === undefined)
2343
+ return zero;
2344
+ const d = recv instanceof Date ? recv : new Date(recv);
2345
+ if (Number.isNaN(d.getTime()))
2346
+ return zero;
2347
+ return d[op]();
2348
+ }
2283
2349
  // src/runtime/insert.ts
2284
2350
  import { createEffect as createEffect3, untrack as untrack2 } from "@barefootjs/client/reactive";
2285
2351
  function makeRegion(scope) {
@@ -2676,6 +2742,7 @@ function render(container, nameOrDef, props = {}) {
2676
2742
  for (const node of Array.from(tpl.content.childNodes)) {
2677
2743
  container.appendChild(node);
2678
2744
  }
2745
+ container.appendChild(document.createComment(`${BF_SCOPE_COMMENT_END_PREFIX}${scopeId}`));
2679
2746
  const proxyEl = rootElements[0];
2680
2747
  commentScopeRegistry.set(proxyEl, { commentNode, scopeId });
2681
2748
  init(proxyEl, props);
@@ -2756,6 +2823,7 @@ export {
2756
2823
  escapeAttr,
2757
2824
  endTurn,
2758
2825
  disposeScope,
2826
+ date,
2759
2827
  cssEscape,
2760
2828
  createSignal2 as createSignal,
2761
2829
  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;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;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
@@ -1174,9 +1188,48 @@ function $cSingle(scope, id) {
1174
1188
  }
1175
1189
  if (scope.matches?.(`[${BF_SCOPE}$="_${cleanId}"]`))
1176
1190
  return scope;
1177
- return null;
1191
+ return findCommentChildScope(scope, parentScopeIds, cleanId);
1192
+ }
1193
+ return findChildScope(scope, `[${BF_SCOPE}$="_${cleanId}"]`) ?? findCommentChildScope(scope, [], cleanId);
1194
+ }
1195
+ function findCommentChildScope(scope, parentIds, slotId) {
1196
+ for (const comment of commentsInScope(scope)) {
1197
+ const id = parseCommentScopeId(comment.nodeValue ?? "", BF_SCOPE_COMMENT_PREFIX);
1198
+ if (!id)
1199
+ continue;
1200
+ const matches = parentIds.length > 0 ? parentIds.some((parentId) => id === `${parentId}_${slotId}`) : id.endsWith(`_${slotId}`) && !NESTED_SLOT_SUFFIX.test(id);
1201
+ if (!matches)
1202
+ continue;
1203
+ const proxyEl = nextElementSibling(comment) ?? comment.parentElement;
1204
+ if (!proxyEl)
1205
+ return null;
1206
+ commentScopeRegistry.set(proxyEl, { commentNode: comment, scopeId: id });
1207
+ return proxyEl;
1208
+ }
1209
+ return null;
1210
+ }
1211
+ function* commentsInScope(scope) {
1212
+ const commentInfo = commentScopeRegistry.get(scope);
1213
+ if (commentInfo) {
1214
+ const boundary = getCommentScopeBoundary(commentInfo.commentNode);
1215
+ let node = commentInfo.commentNode.nextSibling;
1216
+ while (node && node !== boundary) {
1217
+ if (node.nodeType === Node.COMMENT_NODE) {
1218
+ yield node;
1219
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
1220
+ yield* commentsUnder(node);
1221
+ }
1222
+ node = node.nextSibling;
1223
+ }
1224
+ return;
1225
+ }
1226
+ yield* commentsUnder(scope);
1227
+ }
1228
+ function* commentsUnder(root) {
1229
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_COMMENT);
1230
+ while (walker.nextNode()) {
1231
+ yield walker.currentNode;
1178
1232
  }
1179
- return findChildScope(scope, `[${BF_SCOPE}$="_${cleanId}"]`);
1180
1233
  }
1181
1234
  function findChildScope(scope, selector) {
1182
1235
  if (scope.matches?.(selector))
@@ -1276,7 +1329,10 @@ function findSsrScopeBySlotIn(parent, slotId, anchorScope, selfMatch) {
1276
1329
  const suffixSelector = `[${BF_SCOPE}$="_${slotId}"]`;
1277
1330
  if (selfMatch && parent.matches(suffixSelector))
1278
1331
  return parent;
1279
- return parent.querySelector(suffixSelector);
1332
+ const bySuffix = parent.querySelector(suffixSelector);
1333
+ if (bySuffix)
1334
+ return bySuffix;
1335
+ return findCommentChildScope(parent, parentBfs ? [parentBfs] : [], slotId);
1280
1336
  }
1281
1337
 
1282
1338
  // src/runtime/registry.ts
@@ -2682,6 +2738,16 @@ function spreadAttrs(obj) {
2682
2738
  }
2683
2739
  return parts.join(" ");
2684
2740
  }
2741
+ // src/runtime/date.ts
2742
+ function date(recv, op) {
2743
+ const zero = op === "toISOString" ? "" : 0;
2744
+ if (recv === null || recv === undefined)
2745
+ return zero;
2746
+ const d = recv instanceof Date ? recv : new Date(recv);
2747
+ if (Number.isNaN(d.getTime()))
2748
+ return zero;
2749
+ return d[op]();
2750
+ }
2685
2751
  // src/runtime/insert.ts
2686
2752
  function makeRegion(scope) {
2687
2753
  if (scope.nodeType === Node.COMMENT_NODE) {
@@ -3076,6 +3142,7 @@ function render(container, nameOrDef, props = {}) {
3076
3142
  for (const node of Array.from(tpl.content.childNodes)) {
3077
3143
  container.appendChild(node);
3078
3144
  }
3145
+ container.appendChild(document.createComment(`${BF_SCOPE_COMMENT_END_PREFIX}${scopeId}`));
3079
3146
  const proxyEl = rootElements[0];
3080
3147
  commentScopeRegistry.set(proxyEl, { commentNode, scopeId });
3081
3148
  init(proxyEl, props);
@@ -3156,6 +3223,7 @@ export {
3156
3223
  escapeAttr,
3157
3224
  endTurn,
3158
3225
  disposeScope,
3226
+ date,
3159
3227
  cssEscape,
3160
3228
  createSignal,
3161
3229
  createSelector,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/client",
3
- "version": "0.19.1",
3
+ "version": "0.21.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.19.1"
58
+ "@barefootjs/shared": "0.21.0"
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'
@@ -492,11 +492,91 @@ function $cSingle(scope: Element | null, id: string): Element | null {
492
492
  // Precise match found nothing. Check if scope itself matches the short suffix
493
493
  // (fragment root / inlined component where scope IS the child).
494
494
  if (scope.matches?.(`[${BF_SCOPE}$="_${cleanId}"]`)) return scope
495
- return null
495
+ // Fragment-root child: no element carries the child's scope id — it is
496
+ // declared by a bf-scope: comment instead (#2289).
497
+ return findCommentChildScope(scope, parentScopeIds, cleanId)
496
498
  }
497
499
 
498
500
  // Fallback: no parent scope ID available — use short suffix match (best-effort)
499
501
  return findChildScope(scope, `[${BF_SCOPE}$="_${cleanId}"]`)
502
+ ?? findCommentChildScope(scope, [], cleanId)
503
+ }
504
+
505
+ /**
506
+ * Resolve a comment-anchored child scope (fragment-root child, #2289).
507
+ *
508
+ * A child component whose root is a JSX Fragment has no element carrying
509
+ * `bf-s`/`bf-h`/`bf-m` — its scope is declared by a
510
+ * `<!--bf-scope:<parentId>_<slotId>|h=...|m=...-->` comment. The element
511
+ * selectors in `$cSingle` / `findSsrScopeBySlotIn` can never match it, so
512
+ * without this fallback the parent's `initChild(...)` receives `null` and
513
+ * the child is never initialized: every callback prop dies silently and
514
+ * no reactive update ever reaches the child.
515
+ *
516
+ * Matches on the scope id embedded in the comment (`<parentId>_<slotId>`,
517
+ * the same convention the element path's `[bf-s$="<parentId>_<slotId>"]`
518
+ * selector relies on) rather than on the `|h=`/`|m=` segments — the scope
519
+ * id is the first `|`-free token, so matching it cannot be confused by
520
+ * props JSON that happens to contain `|h=`.
521
+ *
522
+ * On match, the proxy element (first element sibling after the comment,
523
+ * falling back to the comment's parent) is registered in
524
+ * `commentScopeRegistry` so the child's own `$`/`$t` queries walk the
525
+ * comment range — the same wiring `hydrateCommentScope` performs for
526
+ * walker-owned root fragments.
527
+ */
528
+ export function findCommentChildScope(
529
+ scope: Element,
530
+ parentIds: readonly string[],
531
+ slotId: string,
532
+ ): Element | null {
533
+ for (const comment of commentsInScope(scope)) {
534
+ const id = parseCommentScopeId(comment.nodeValue ?? '', BF_SCOPE_COMMENT_PREFIX)
535
+ if (!id) continue
536
+ const matches = parentIds.length > 0
537
+ ? parentIds.some(parentId => id === `${parentId}_${slotId}`)
538
+ : id.endsWith(`_${slotId}`) && !NESTED_SLOT_SUFFIX.test(id)
539
+ if (!matches) continue
540
+
541
+ const proxyEl = nextElementSibling(comment) ?? comment.parentElement
542
+ if (!proxyEl) return null
543
+ commentScopeRegistry.set(proxyEl, { commentNode: comment, scopeId: id })
544
+ return proxyEl
545
+ }
546
+ return null
547
+ }
548
+
549
+ /**
550
+ * Enumerate comment nodes in a scope's DOM range: the comment-scope
551
+ * sibling range for comment-anchored scopes, the element subtree
552
+ * otherwise. Mirrors candidatesInScope's notion of "where the scope's
553
+ * content lives", but yields comments instead of elements.
554
+ */
555
+ function* commentsInScope(scope: Element): Generator<Comment> {
556
+ const commentInfo = commentScopeRegistry.get(scope)
557
+
558
+ if (commentInfo) {
559
+ const boundary = getCommentScopeBoundary(commentInfo.commentNode)
560
+ let node: Node | null = commentInfo.commentNode.nextSibling
561
+ while (node && node !== boundary) {
562
+ if (node.nodeType === Node.COMMENT_NODE) {
563
+ yield node as Comment
564
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
565
+ yield* commentsUnder(node as Element)
566
+ }
567
+ node = node.nextSibling
568
+ }
569
+ return
570
+ }
571
+
572
+ yield* commentsUnder(scope)
573
+ }
574
+
575
+ function* commentsUnder(root: Element): Generator<Comment> {
576
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_COMMENT)
577
+ while (walker.nextNode()) {
578
+ yield walker.currentNode as Comment
579
+ }
500
580
  }
501
581
 
502
582
  /**
@@ -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
  }