@barefootjs/client 0.5.2 → 0.6.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.
@@ -1 +1 @@
1
- {"version":3,"file":"apply-rest-attrs.d.ts","sourceRoot":"","sources":["../../src/runtime/apply-rest-attrs.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAiBH;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,EAAE,EAAE,OAAO,EACX,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,WAAW,EAAE,MAAM,EAAE,GACpB,IAAI,CA4DN"}
1
+ {"version":3,"file":"apply-rest-attrs.d.ts","sourceRoot":"","sources":["../../src/runtime/apply-rest-attrs.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAiBH;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,EAAE,EAAE,OAAO,EACX,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,WAAW,EAAE,MAAM,EAAE,GACpB,IAAI,CAkEN"}
@@ -1,22 +1,2 @@
1
- /**
2
- * Branch-template slot helper (#1213).
3
- *
4
- * Conditional `template()` arrows interpolate Child-position expressions
5
- * via `${expr}`. When `expr` evaluates to a live `Node` (e.g. the result
6
- * of `_p.renderNode(node())` returning an `HTMLElement` from
7
- * `createComponent`), the surrounding template literal coerces it via
8
- * `Object.prototype.toString`, producing `"[object HTMLDivElement]"` and
9
- * destroying the live node identity on hydration.
10
- *
11
- * `__bfSlot` intercepts the value before stringification: if it's a
12
- * `Node`, it stashes the node into the closure-scoped `slots` array and
13
- * returns a unique marker comment. The `insert()` runtime then walks the
14
- * parsed fragment for those markers and splices the original node back
15
- * in by identity (no `cloneNode`), preserving event listeners and signal
16
- * bindings.
17
- *
18
- * Non-node values fall through to `String(value)` for the existing
19
- * inline-string path.
20
- */
21
1
  export declare function __bfSlot(value: unknown, slots: Node[]): string;
22
2
  //# sourceMappingURL=branch-slot.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"branch-slot.d.ts","sourceRoot":"","sources":["../../src/runtime/branch-slot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,MAAM,CAW9D"}
1
+ {"version":3,"file":"branch-slot.d.ts","sourceRoot":"","sources":["../../src/runtime/branch-slot.ts"],"names":[],"mappings":"AA2BA,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,MAAM,CAW9D"}
@@ -81,6 +81,28 @@ export declare function renderChild(name: string, props: Record<string, unknown>
81
81
  * back to ">" in the DOM attribute value, preserving CSS matching.
82
82
  */
83
83
  export declare function escapeAttrGt(html: string): string;
84
+ /**
85
+ * HTML-escape a single attribute *value* before it is concatenated into a
86
+ * client-rendered template string. Matches the SSR adapters' attribute
87
+ * escaping (Hono's `escapeToBuffer`: `& " ' < >`) so client-rendered DOM
88
+ * is byte-identical to the server-rendered form and metacharacter-bearing
89
+ * values (UnoCSS `[class*="size-"]`, `has-[>svg]`, …) don't corrupt
90
+ * attribute parsing when the template is inserted via `innerHTML`. `&` is
91
+ * replaced first so the emitted entities aren't themselves re-escaped.
92
+ */
93
+ export declare function escapeAttr(value: unknown): string;
94
+ /**
95
+ * HTML-escape interpolated **text content** before it is concatenated into
96
+ * a client-rendered template string (the `<!--bf:sN-->…<!--/-->` text
97
+ * slots). The HTML spec only requires `& < >` in text, but the SSR
98
+ * adapters (Hono) escape text with the same set as attribute values
99
+ * (`& " ' < >`), and the fixture-hydrate / CSR-conformance layer requires
100
+ * byte-parity with the server-rendered output — so this delegates to
101
+ * `escapeAttr`. Kept as a distinct export so generated code reads
102
+ * `escapeText(...)` at text sites (self-documenting) and so the two
103
+ * contexts can diverge later without touching call sites.
104
+ */
105
+ export declare function escapeText(value: unknown): string;
84
106
  /**
85
107
  * Parse an HTML string into a DocumentFragment, safely escaping ">" in
86
108
  * attribute values. All code that sets innerHTML on dynamic HTML should
@@ -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,SAAS,CAAA;AAS3C,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAExD;AAWD;;;;;;;;;;;;;;;;;;;;GAoBG;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,GAC7B,WAAW,CAyIb;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAE3F;AAuBD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,WAAW,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC,GAAG,SAAS,CAE7G;AAGD;;;;;;;;;;;;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;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,SAAS,CAAA;AAS3C,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAExD;AAWD;;;;;;;;;;;;;;;;;;;;GAoBG;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,GAC7B,WAAW,CAgLb;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAE3F;AAuBD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,WAAW,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC,GAAG,SAAS,CAE7G;AAGD;;;;;;;;;;;;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;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEjD;AAID;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,GAAG,gBAAgB,CAcjF"}
@@ -10,7 +10,7 @@ export { reconcileElements, getLoopChildren, getLoopNodes } from './reconcile-el
10
10
  export { qsaItem, upsertChildItem } from './qsa-item';
11
11
  export { mapArray, mapArrayAnchored } from './map-array';
12
12
  export { registerTemplate, getTemplate, hasTemplate, type TemplateFn } from './template';
13
- export { createComponent, renderChild, getPropsUpdateFn, getComponentProps, parseHTML, } from './component';
13
+ export { createComponent, renderChild, getPropsUpdateFn, getComponentProps, parseHTML, escapeAttr, escapeText, } from './component';
14
14
  export { applyRestAttrs } from './apply-rest-attrs';
15
15
  export { spreadAttrs } from './spread-attrs';
16
16
  export { styleToCss } from './style';
@@ -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,UAAU,EACV,SAAS,EACT,OAAO,EACP,OAAO,EACP,KAAK,EACL,KAAK,QAAQ,EACb,KAAK,MAAM,EACX,KAAK,IAAI,EACT,KAAK,SAAS,EACd,KAAK,QAAQ,GACd,MAAM,6BAA6B,CAAA;AAEpC,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAC3C,OAAO,EAAE,MAAM,EAAE,KAAK,UAAU,EAAE,MAAM,SAAS,CAAA;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC/C,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAA;AAIlC,OAAO,EACL,aAAa,EACb,UAAU,EACV,cAAc,EACd,eAAe,EACf,KAAK,OAAO,GACb,MAAM,WAAW,CAAA;AAGlB,OAAO,EACL,YAAY,EACZ,WAAW,EACX,eAAe,EACf,wBAAwB,EACxB,KAAK,MAAM,EACX,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,cAAc,GACpB,MAAM,UAAU,CAAA;AAGjB,OAAO,EAAE,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,QAAQ,CAAA;AACzD,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AACvF,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AACrD,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAGxD,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,WAAW,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAA;AAGxF,OAAO,EACL,eAAe,EACf,WAAW,EACX,gBAAgB,EAChB,iBAAiB,EACjB,SAAS,GACV,MAAM,aAAa,CAAA;AAGpB,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAGpC,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,aAAa,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AACnG,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAA;AACnF,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AACxF,OAAO,EAAE,MAAM,EAAE,KAAK,YAAY,EAAE,KAAK,oBAAoB,EAAE,MAAM,UAAU,CAAA;AAC/E,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AACzC,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAGpD,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAGlD,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAGjC,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAGvD,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA"}
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,UAAU,EACV,SAAS,EACT,OAAO,EACP,OAAO,EACP,KAAK,EACL,KAAK,QAAQ,EACb,KAAK,MAAM,EACX,KAAK,IAAI,EACT,KAAK,SAAS,EACd,KAAK,QAAQ,GACd,MAAM,6BAA6B,CAAA;AAEpC,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAC3C,OAAO,EAAE,MAAM,EAAE,KAAK,UAAU,EAAE,MAAM,SAAS,CAAA;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC/C,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAA;AAIlC,OAAO,EACL,aAAa,EACb,UAAU,EACV,cAAc,EACd,eAAe,EACf,KAAK,OAAO,GACb,MAAM,WAAW,CAAA;AAGlB,OAAO,EACL,YAAY,EACZ,WAAW,EACX,eAAe,EACf,wBAAwB,EACxB,KAAK,MAAM,EACX,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,cAAc,GACpB,MAAM,UAAU,CAAA;AAGjB,OAAO,EAAE,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,QAAQ,CAAA;AACzD,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AACvF,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AACrD,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAGxD,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,WAAW,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAA;AAGxF,OAAO,EACL,eAAe,EACf,WAAW,EACX,gBAAgB,EAChB,iBAAiB,EACjB,SAAS,EACT,UAAU,EACV,UAAU,GACX,MAAM,aAAa,CAAA;AAGpB,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAGpC,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,aAAa,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AACnG,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAA;AACnF,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AACxF,OAAO,EAAE,MAAM,EAAE,KAAK,YAAY,EAAE,KAAK,oBAAoB,EAAE,MAAM,UAAU,CAAA;AAC/E,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AACzC,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAGpD,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAGlD,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAGjC,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAGvD,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA"}
@@ -123,6 +123,7 @@ function loopEndMarker(markerId) {
123
123
  return `${BF_LOOP_END}:${markerId}`;
124
124
  }
125
125
  var BF_KEY = "data-key";
126
+ var BF_PLACEHOLDER = "data-bf-ph";
126
127
  var BF_ASYNC = "bf-async";
127
128
  var BF_ASYNC_RESOLVE = "bf-async-resolve";
128
129
  var BF_PARENT_SCOPE_PLACEHOLDER = "__BF_PARENT_SCOPE__";
@@ -248,6 +249,8 @@ function classifyDOMProp(key) {
248
249
  return { kind: "skip", attrName: key };
249
250
  if (key === "ref")
250
251
  return { kind: "ref", attrName: key };
252
+ if (key === "dangerouslySetInnerHTML")
253
+ return { kind: "innerHTML", attrName: key };
251
254
  if (isEventProp(key))
252
255
  return { kind: "event", attrName: key };
253
256
  const attrName = toHTMLAttrNameRuntime(key);
@@ -791,7 +794,7 @@ function upsertChild(parent, name, slotId, props, key, anchorScope) {
791
794
  return ssr;
792
795
  }
793
796
  const phId = slotId ?? name;
794
- const ph = parent.querySelector(`[data-bf-ph="${phId}"]`);
797
+ const ph = parent.getAttribute(BF_PLACEHOLDER) === phId ? parent : parent.querySelector(`[${BF_PLACEHOLDER}="${phId}"]`);
795
798
  if (ph) {
796
799
  const slot = slotId ? buildSlotInfo(parent, slotId, anchorScope) : undefined;
797
800
  const comp = createComponent(name, props, key, slot);
@@ -1017,10 +1020,24 @@ function createComponent(nameOrDef, props = {}, key, slot) {
1017
1020
  element.setAttribute(BF_KEY, String(key));
1018
1021
  }
1019
1022
  const prevScope = setCurrentScope(element);
1023
+ const rootIsDeferredPlaceholder = element.hasAttribute(BF_PLACEHOLDER);
1024
+ let placeholderWrapper = null;
1025
+ if (rootIsDeferredPlaceholder) {
1026
+ placeholderWrapper = parseHTML("<div></div>").firstChild;
1027
+ placeholderWrapper.appendChild(element);
1028
+ }
1020
1029
  const initFn = getComponentInit(name);
1021
1030
  if (initFn) {
1022
1031
  initFn(element, props);
1023
1032
  }
1033
+ if (rootIsDeferredPlaceholder && placeholderWrapper) {
1034
+ const materialised = placeholderWrapper.firstElementChild;
1035
+ if (materialised && !materialised.hasAttribute(BF_PLACEHOLDER)) {
1036
+ setCurrentScope(prevScope);
1037
+ return materialised;
1038
+ }
1039
+ placeholderWrapper.removeChild(element);
1040
+ }
1024
1041
  if (childrenIsGetter) {
1025
1042
  const children = untrack(() => childrenDescriptor.get());
1026
1043
  if (children != null) {
@@ -1104,6 +1121,12 @@ function unwrapPropsForTemplate(props) {
1104
1121
  function escapeAttrGt(html) {
1105
1122
  return html.replace(/"[^"]*"/g, (match) => match.replace(/>/g, "&gt;"));
1106
1123
  }
1124
+ function escapeAttr(value) {
1125
+ return String(value).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1126
+ }
1127
+ function escapeText(value) {
1128
+ return escapeAttr(value);
1129
+ }
1107
1130
  var SVG_NS = "http://www.w3.org/2000/svg";
1108
1131
  function parseHTML(html, parent) {
1109
1132
  const tpl = document.createElement("template");
@@ -1932,7 +1955,10 @@ function applyRestAttrs(el, source, excludeKeys) {
1932
1955
  createEffect2(() => {
1933
1956
  for (const { key, c } of attrEntries) {
1934
1957
  const value = source[key];
1935
- if (value != null && value !== false) {
1958
+ if (c.kind === "innerHTML") {
1959
+ const html = value;
1960
+ el.innerHTML = html != null && html.__html != null ? String(html.__html) : "";
1961
+ } else if (value != null && value !== false) {
1936
1962
  if (c.kind === "property" && c.attrName === "value" && "value" in el) {
1937
1963
  const strVal = String(value);
1938
1964
  if (el.value !== strVal)
@@ -1975,6 +2001,8 @@ function spreadAttrs(obj) {
1975
2001
  const c = classifyDOMProp(key);
1976
2002
  if (c.kind === "event" || c.kind === "skip" || c.kind === "ref")
1977
2003
  continue;
2004
+ if (c.kind === "innerHTML")
2005
+ continue;
1978
2006
  if (c.kind === "style") {
1979
2007
  const css = styleToCss(value);
1980
2008
  if (css != null)
@@ -2251,7 +2279,7 @@ function __bfSlot(value, slots) {
2251
2279
  if (Array.isArray(value)) {
2252
2280
  return value.map((v) => __bfSlot(v, slots)).join("");
2253
2281
  }
2254
- return String(value);
2282
+ return escapeText(value);
2255
2283
  }
2256
2284
  // src/runtime/dynamic-text.ts
2257
2285
  var END_MARKER = "/";
@@ -2445,6 +2473,8 @@ export {
2445
2473
  findSiblingSlot,
2446
2474
  findScope,
2447
2475
  find,
2476
+ escapeText,
2477
+ escapeAttr,
2448
2478
  cssEscape,
2449
2479
  createSignal2 as createSignal,
2450
2480
  createRoot2 as createRoot,
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/runtime/registry.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAarC;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAclE;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEjE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,OAAO,GAAG,IAAI,EAC1B,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAClC,IAAI,CAoCN;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,OAAO,EACf,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,CAwBpB"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/runtime/registry.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAarC;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAclE;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEjE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,OAAO,GAAG,IAAI,EAC1B,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAClC,IAAI,CAoCN;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,OAAO,EACf,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,CA8BpB"}
@@ -1 +1 @@
1
- {"version":3,"file":"spread-attrs.d.ts","sourceRoot":"","sources":["../../src/runtime/spread-attrs.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAmBhE"}
1
+ {"version":3,"file":"spread-attrs.d.ts","sourceRoot":"","sources":["../../src/runtime/spread-attrs.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAyBhE"}
@@ -314,6 +314,7 @@ function loopEndMarker(markerId) {
314
314
  return `${BF_LOOP_END}:${markerId}`;
315
315
  }
316
316
  var BF_KEY = "data-key";
317
+ var BF_PLACEHOLDER = "data-bf-ph";
317
318
  var BF_ASYNC = "bf-async";
318
319
  var BF_ASYNC_RESOLVE = "bf-async-resolve";
319
320
  var BF_PARENT_SCOPE_PLACEHOLDER = "__BF_PARENT_SCOPE__";
@@ -439,6 +440,8 @@ function classifyDOMProp(key) {
439
440
  return { kind: "skip", attrName: key };
440
441
  if (key === "ref")
441
442
  return { kind: "ref", attrName: key };
443
+ if (key === "dangerouslySetInnerHTML")
444
+ return { kind: "innerHTML", attrName: key };
442
445
  if (isEventProp(key))
443
446
  return { kind: "event", attrName: key };
444
447
  const attrName = toHTMLAttrNameRuntime(key);
@@ -982,7 +985,7 @@ function upsertChild(parent, name, slotId, props, key, anchorScope) {
982
985
  return ssr;
983
986
  }
984
987
  const phId = slotId ?? name;
985
- const ph = parent.querySelector(`[data-bf-ph="${phId}"]`);
988
+ const ph = parent.getAttribute(BF_PLACEHOLDER) === phId ? parent : parent.querySelector(`[${BF_PLACEHOLDER}="${phId}"]`);
986
989
  if (ph) {
987
990
  const slot = slotId ? buildSlotInfo(parent, slotId, anchorScope) : undefined;
988
991
  const comp = createComponent(name, props, key, slot);
@@ -1207,10 +1210,24 @@ function createComponent(nameOrDef, props = {}, key, slot) {
1207
1210
  element.setAttribute(BF_KEY, String(key));
1208
1211
  }
1209
1212
  const prevScope = setCurrentScope(element);
1213
+ const rootIsDeferredPlaceholder = element.hasAttribute(BF_PLACEHOLDER);
1214
+ let placeholderWrapper = null;
1215
+ if (rootIsDeferredPlaceholder) {
1216
+ placeholderWrapper = parseHTML("<div></div>").firstChild;
1217
+ placeholderWrapper.appendChild(element);
1218
+ }
1210
1219
  const initFn = getComponentInit(name);
1211
1220
  if (initFn) {
1212
1221
  initFn(element, props);
1213
1222
  }
1223
+ if (rootIsDeferredPlaceholder && placeholderWrapper) {
1224
+ const materialised = placeholderWrapper.firstElementChild;
1225
+ if (materialised && !materialised.hasAttribute(BF_PLACEHOLDER)) {
1226
+ setCurrentScope(prevScope);
1227
+ return materialised;
1228
+ }
1229
+ placeholderWrapper.removeChild(element);
1230
+ }
1214
1231
  if (childrenIsGetter) {
1215
1232
  const children = untrack(() => childrenDescriptor.get());
1216
1233
  if (children != null) {
@@ -1294,6 +1311,12 @@ function unwrapPropsForTemplate(props) {
1294
1311
  function escapeAttrGt(html) {
1295
1312
  return html.replace(/"[^"]*"/g, (match) => match.replace(/>/g, "&gt;"));
1296
1313
  }
1314
+ function escapeAttr(value) {
1315
+ return String(value).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1316
+ }
1317
+ function escapeText(value) {
1318
+ return escapeAttr(value);
1319
+ }
1297
1320
  var SVG_NS = "http://www.w3.org/2000/svg";
1298
1321
  function parseHTML(html, parent) {
1299
1322
  const tpl = document.createElement("template");
@@ -2118,7 +2141,10 @@ function applyRestAttrs(el, source, excludeKeys) {
2118
2141
  createEffect(() => {
2119
2142
  for (const { key, c } of attrEntries) {
2120
2143
  const value = source[key];
2121
- if (value != null && value !== false) {
2144
+ if (c.kind === "innerHTML") {
2145
+ const html = value;
2146
+ el.innerHTML = html != null && html.__html != null ? String(html.__html) : "";
2147
+ } else if (value != null && value !== false) {
2122
2148
  if (c.kind === "property" && c.attrName === "value" && "value" in el) {
2123
2149
  const strVal = String(value);
2124
2150
  if (el.value !== strVal)
@@ -2161,6 +2187,8 @@ function spreadAttrs(obj) {
2161
2187
  const c = classifyDOMProp(key);
2162
2188
  if (c.kind === "event" || c.kind === "skip" || c.kind === "ref")
2163
2189
  continue;
2190
+ if (c.kind === "innerHTML")
2191
+ continue;
2164
2192
  if (c.kind === "style") {
2165
2193
  const css = styleToCss(value);
2166
2194
  if (css != null)
@@ -2436,7 +2464,7 @@ function __bfSlot(value, slots) {
2436
2464
  if (Array.isArray(value)) {
2437
2465
  return value.map((v) => __bfSlot(v, slots)).join("");
2438
2466
  }
2439
- return String(value);
2467
+ return escapeText(value);
2440
2468
  }
2441
2469
  // src/runtime/dynamic-text.ts
2442
2470
  var END_MARKER = "/";
@@ -2630,6 +2658,8 @@ export {
2630
2658
  findSiblingSlot,
2631
2659
  findScope,
2632
2660
  find,
2661
+ escapeText,
2662
+ escapeAttr,
2633
2663
  cssEscape,
2634
2664
  createSignal,
2635
2665
  createRoot,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/client",
3
- "version": "0.5.2",
3
+ "version": "0.6.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.5.2"
58
+ "@barefootjs/shared": "0.6.0"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "@barefootjs/jsx": ">=0.2.0"
@@ -64,7 +64,13 @@ export function applyRestAttrs(
64
64
  for (const { key, c } of attrEntries) {
65
65
  const value = source[key]
66
66
 
67
- if (value != null && value !== false) {
67
+ if (c.kind === 'innerHTML') {
68
+ // `dangerouslySetInnerHTML={{ __html }}` arriving via a rest object —
69
+ // set the element's raw innerHTML (the escape hatch), never an
70
+ // attribute. Reactive: re-runs when `source` changes.
71
+ const html = value as { __html?: unknown } | null | undefined
72
+ el.innerHTML = html != null && html.__html != null ? String(html.__html) : ''
73
+ } else if (value != null && value !== false) {
68
74
  if (c.kind === 'property' && c.attrName === 'value' && 'value' in el) {
69
75
  const strVal = String(value)
70
76
  if ((el as HTMLInputElement).value !== strVal) (el as HTMLInputElement).value = strVal
@@ -15,9 +15,16 @@
15
15
  * in by identity (no `cloneNode`), preserving event listeners and signal
16
16
  * bindings.
17
17
  *
18
- * Non-node values fall through to `String(value)` for the existing
19
- * inline-string path.
18
+ * Non-node values fall through to the inline-string path, HTML-escaped
19
+ * (#1694 follow-up) so a branch-template text value containing `< / &`
20
+ * surfaces as text — not markup — and matches the SSR-rendered bytes.
21
+ * Escaping happens here, on the string path only, so the `<!--bf-slot:N-->`
22
+ * markers returned for live `Node` values are left intact for `insert()`
23
+ * to splice. (Doing it here rather than wrapping the whole `__bfSlot(...)`
24
+ * call in `escapeText` is what lets the marker path stay raw.)
20
25
  */
26
+ import { escapeText } from './component'
27
+
21
28
  export function __bfSlot(value: unknown, slots: Node[]): string {
22
29
  if (value == null || value === false || value === true) return ''
23
30
  if (typeof Node !== 'undefined' && value instanceof Node) {
@@ -28,5 +35,5 @@ export function __bfSlot(value: unknown, slots: Node[]): string {
28
35
  if (Array.isArray(value)) {
29
36
  return value.map((v) => __bfSlot(v, slots)).join('')
30
37
  }
31
- return String(value)
38
+ return escapeText(value)
32
39
  }
@@ -11,7 +11,7 @@ import { getRegisteredDef } from './hydrate'
11
11
  import { hydratedScopes } from './hydration-state'
12
12
  import { untrack } from '@barefootjs/client/reactive'
13
13
  import { setCurrentScope } from './context'
14
- import { BF_SCOPE, BF_KEY, BF_HOST, BF_AT, BF_PARENT_SCOPE_PLACEHOLDER } from '@barefootjs/shared'
14
+ import { BF_SCOPE, BF_KEY, BF_HOST, BF_AT, BF_PARENT_SCOPE_PLACEHOLDER, BF_PLACEHOLDER } from '@barefootjs/shared'
15
15
  import type { ComponentDef } from './types'
16
16
 
17
17
  // Parent scope ID context for renderChild() inside insert() branch templates.
@@ -186,13 +186,52 @@ export function createComponent(
186
186
  // This allows context providers in initFn to store context on this element.
187
187
  const prevScope = setCurrentScope(element)
188
188
 
189
+ // 8b. Root-level deferred child (dropped-prop fix): a comment-wrapper
190
+ // parent whose entire render is a single deferred child renders as a
191
+ // bare `data-bf-ph` placeholder. The parent's init calls
192
+ // `upsertChild(__scope, ...)` which replaces the placeholder via
193
+ // `replaceWith` — but a detached root node can't replace itself in
194
+ // place. Park it in a throwaway wrapper so the replacement lands
195
+ // somewhere we can recover, then return the materialised child.
196
+ const rootIsDeferredPlaceholder = element.hasAttribute(BF_PLACEHOLDER)
197
+ let placeholderWrapper: HTMLElement | null = null
198
+ if (rootIsDeferredPlaceholder) {
199
+ placeholderWrapper = parseHTML('<div></div>').firstChild as HTMLElement
200
+ placeholderWrapper.appendChild(element)
201
+ }
202
+
189
203
  // 9. Initialize the component (context providers set up here).
190
204
  const initFn = getComponentInit(name)
191
205
  if (initFn) {
192
- // Pass original props (with getters) for reactivity
206
+ // Pass original props (with getters) for reactivity. For a root
207
+ // deferred placeholder, init's `upsertChild(element, ...)` matches the
208
+ // placeholder element itself and replaces it inside the wrapper.
193
209
  initFn(element, props)
194
210
  }
195
211
 
212
+ if (rootIsDeferredPlaceholder && placeholderWrapper) {
213
+ const materialised = placeholderWrapper.firstElementChild as HTMLElement | null
214
+ if (materialised && !materialised.hasAttribute(BF_PLACEHOLDER)) {
215
+ // The deferred child was created in place of the placeholder.
216
+ // `materialised` is the child's OWN element, created via
217
+ // upsertChild -> createComponent, which already registered itself
218
+ // (hydratedScopes / propsMap / registerPropsUpdate) keyed to the
219
+ // child with the child's own props. We must NOT re-register it here:
220
+ // overwriting propsMap/registerPropsUpdate with the *parent's* props
221
+ // would mis-key the child (e.g. a later getComponentProps would read
222
+ // the parent's props), and re-running the parent's init on an element
223
+ // whose placeholder is already gone could not re-materialise. So just
224
+ // restore the scope and return the already-registered child.
225
+ // (Parent-scope effects are unaffected: createEffect ownership lives
226
+ // in the EffectContext tree, not the discarded placeholder element.)
227
+ setCurrentScope(prevScope)
228
+ return materialised
229
+ }
230
+ // Placeholder was not replaced (no init / no matching child): fall
231
+ // through with the original placeholder element detached from wrapper.
232
+ placeholderWrapper.removeChild(element)
233
+ }
234
+
196
235
  // 10. Evaluate getter children and insert them.
197
236
  // Children are evaluated NOW (after initFn) so that context provided by
198
237
  // the parent is in the global store when children call useContext().
@@ -400,6 +439,39 @@ export function escapeAttrGt(html: string): string {
400
439
  return html.replace(/"[^"]*"/g, match => match.replace(/>/g, '&gt;'))
401
440
  }
402
441
 
442
+ /**
443
+ * HTML-escape a single attribute *value* before it is concatenated into a
444
+ * client-rendered template string. Matches the SSR adapters' attribute
445
+ * escaping (Hono's `escapeToBuffer`: `& " ' < >`) so client-rendered DOM
446
+ * is byte-identical to the server-rendered form and metacharacter-bearing
447
+ * values (UnoCSS `[class*="size-"]`, `has-[>svg]`, …) don't corrupt
448
+ * attribute parsing when the template is inserted via `innerHTML`. `&` is
449
+ * replaced first so the emitted entities aren't themselves re-escaped.
450
+ */
451
+ export function escapeAttr(value: unknown): string {
452
+ return String(value)
453
+ .replace(/&/g, '&amp;')
454
+ .replace(/"/g, '&quot;')
455
+ .replace(/'/g, '&#39;')
456
+ .replace(/</g, '&lt;')
457
+ .replace(/>/g, '&gt;')
458
+ }
459
+
460
+ /**
461
+ * HTML-escape interpolated **text content** before it is concatenated into
462
+ * a client-rendered template string (the `<!--bf:sN-->…<!--/-->` text
463
+ * slots). The HTML spec only requires `& < >` in text, but the SSR
464
+ * adapters (Hono) escape text with the same set as attribute values
465
+ * (`& " ' < >`), and the fixture-hydrate / CSR-conformance layer requires
466
+ * byte-parity with the server-rendered output — so this delegates to
467
+ * `escapeAttr`. Kept as a distinct export so generated code reads
468
+ * `escapeText(...)` at text sites (self-documenting) and so the two
469
+ * contexts can diverge later without touching call sites.
470
+ */
471
+ export function escapeText(value: unknown): string {
472
+ return escapeAttr(value)
473
+ }
474
+
403
475
  const SVG_NS = 'http://www.w3.org/2000/svg'
404
476
 
405
477
  /**
@@ -68,6 +68,8 @@ export {
68
68
  getPropsUpdateFn,
69
69
  getComponentProps,
70
70
  parseHTML,
71
+ escapeAttr,
72
+ escapeText,
71
73
  } from './component'
72
74
 
73
75
  // Spread props helpers
@@ -5,7 +5,7 @@
5
5
  * Each component registers its init function so parents can initialize children with props.
6
6
  */
7
7
 
8
- import { BF_SCOPE, BF_HOST } from '@barefootjs/shared'
8
+ import { BF_SCOPE, BF_HOST, BF_PLACEHOLDER } from '@barefootjs/shared'
9
9
  import { hydratedScopes } from './hydration-state'
10
10
  import { setCurrentScope } from './context'
11
11
  import { createComponent } from './component'
@@ -148,8 +148,14 @@ export function upsertChild(
148
148
  return ssr
149
149
  }
150
150
  // CSR: replace placeholder with a freshly-created component.
151
+ // The placeholder is normally a descendant of `parent`; but a
152
+ // comment-scope parent whose root IS the deferred child renders the
153
+ // placeholder as `parent` itself (no wrapper element), so match the
154
+ // parent element directly before falling back to a subtree query.
151
155
  const phId = slotId ?? name
152
- const ph = parent.querySelector(`[data-bf-ph="${phId}"]`) as HTMLElement | null
156
+ const ph = (parent.getAttribute(BF_PLACEHOLDER) === phId
157
+ ? (parent as HTMLElement)
158
+ : parent.querySelector(`[${BF_PLACEHOLDER}="${phId}"]`)) as HTMLElement | null
153
159
  if (ph) {
154
160
  const slot = slotId ? buildSlotInfo(parent, slotId, anchorScope) : undefined
155
161
  const comp = createComponent(name, props, key, slot)
@@ -23,6 +23,12 @@ export function spreadAttrs(obj: Record<string, unknown>): string {
23
23
  if (value == null || value === false) continue
24
24
  const c = classifyDOMProp(key)
25
25
  if (c.kind === 'event' || c.kind === 'skip' || c.kind === 'ref') continue
26
+ // `dangerouslySetInnerHTML` is not an attribute — its `{ __html }` is the
27
+ // element's content, which this attribute-string builder can't emit. Skip
28
+ // it so a spread carrying it never serialises a bogus
29
+ // `dangerouslySetInnerHTML="[object Object]"`. (Content is set by the
30
+ // template for an explicit attr, or by `applyRestAttrs` for a rest object.)
31
+ if (c.kind === 'innerHTML') continue
26
32
  if (c.kind === 'style') {
27
33
  const css = styleToCss(value)
28
34
  if (css != null) parts.push(`style="${css}"`)