@barefootjs/jsx 0.31.8 → 0.31.9

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.
Files changed (29) hide show
  1. package/dist/index.js +127 -57
  2. package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
  3. package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
  4. package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
  5. package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts +66 -38
  6. package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts.map +1 -1
  7. package/dist/ir-to-client-js/emit-registration.d.ts.map +1 -1
  8. package/dist/ir-to-client-js/html-template.d.ts +15 -1
  9. package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
  10. package/dist/ir-to-client-js/imports.d.ts +2 -2
  11. package/dist/ir-to-client-js/imports.d.ts.map +1 -1
  12. package/package.json +2 -2
  13. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +210 -210
  14. package/src/__tests__/aliased-destructured-prop-csr.test.ts +8 -2
  15. package/src/__tests__/csr-template-loop-shadowing.test.ts +5 -2
  16. package/src/__tests__/env-signal-template-prelude.test.ts +4 -2
  17. package/src/__tests__/inner-loop-mathml-namespace.test.ts +135 -0
  18. package/src/__tests__/markup-prop-brand.test.ts +161 -0
  19. package/src/__tests__/mathml-mapArray-namespace.test.ts +230 -0
  20. package/src/__tests__/text-slot-escaping.test.ts +21 -12
  21. package/src/ir-to-client-js/collect-elements.ts +38 -5
  22. package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +10 -10
  23. package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +3 -4
  24. package/src/ir-to-client-js/control-flow/stringify/loop.ts +18 -14
  25. package/src/ir-to-client-js/control-flow/stringify/template-parse.ts +142 -78
  26. package/src/ir-to-client-js/emit-registration.ts +5 -1
  27. package/src/ir-to-client-js/html-template.ts +103 -12
  28. package/src/ir-to-client-js/imports.ts +4 -0
  29. package/src/ir-to-client-js/index.ts +6 -1
@@ -321,9 +321,22 @@ function escapeAttrValueExpr(valExpr: string): string {
321
321
  * escaped, so this is applied only at the four text-marker emit sites.
322
322
  * Hono escapes text content with the same set as attribute values
323
323
  * (`& " ' < >`), so `escapeText` delegates to the same operation.
324
+ *
325
+ * `isMarkup` (#2651) switches to `escapeTextOrMarkup` — `escapeText`'s
326
+ * strict superset that additionally unwraps a `bfMarkup()`-branded value
327
+ * raw instead of escaping it. Two of the four text-marker call sites
328
+ * (`irToComponentTemplateWithOpts`, `generateCsrTemplateWithOpts`) pass
329
+ * `true` for a slot `ctx.dynamicElements` also claims — the same
330
+ * membership `emit-reactive.ts` uses to pick the 'markup' writer kind for
331
+ * this slot's REACTIVE update, so the initial-render escape and the
332
+ * reactive-update escape agree on whether the slot may carry raw markup.
333
+ * The other two (`irToHtmlTemplate`, `irToPlaceholderTemplate`) build
334
+ * loop-item / conditional-branch HTML, which the reactive side always
335
+ * treats as plain text (`__bfText`) regardless — they never pass `true`,
336
+ * so they keep calling `escapeText` byte-for-byte as before.
324
337
  */
325
- function escapeTextSlotExpr(innerExpr: string): string {
326
- return `escapeText(${innerExpr})`
338
+ function escapeTextSlotExpr(innerExpr: string, isMarkup = false): string {
339
+ return `${isMarkup ? 'escapeTextOrMarkup' : 'escapeText'}(${innerExpr})`
327
340
  }
328
341
 
329
342
  /**
@@ -901,7 +914,20 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
901
914
  case 'jsx-children': {
902
915
  const hoistedRecurse = (n: IRNode): string => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar, true)
903
916
  const childHtml = p.value.children.map(c => hoistedRecurse(c)).join('')
904
- return `${quotePropName(p.name)}: \`${childHtml}\``
917
+ // Brand the assembled HTML (#2651) — every text segment inside
918
+ // `childHtml` was already escaped node-by-node during
919
+ // `hoistedRecurse`'s own build, so the concatenated result is
920
+ // safe to splice raw; the child's own template evaluates this
921
+ // prop through `escapeTextOrMarkup`/`escapeTextOrNode`, which
922
+ // trust the brand and skip re-escaping it. EXCEPT an explicit
923
+ // `children={<jsx/>}` prop (out of scope, unchanged): the
924
+ // child's `{children}` interpolation is the bare-passthrough
925
+ // door (`escapeTextSlotExpr`'s own docstring), never routed
926
+ // through an escape/unwrap call, so a branded object here
927
+ // would stringify to `[object Object]` instead of unwrapping.
928
+ return p.name === 'children'
929
+ ? `${quotePropName(p.name)}: \`${childHtml}\``
930
+ : `${quotePropName(p.name)}: bfMarkup(\`${childHtml}\`)`
905
931
  }
906
932
  case 'literal':
907
933
  return `${quotePropName(p.name)}: ${JSON.stringify(p.value.value)}`
@@ -1478,6 +1504,25 @@ export function irChildrenToJsExpr(children: IRNode[]): string {
1478
1504
  return `[${exprs.join(', ')}]`
1479
1505
  }
1480
1506
 
1507
+ /**
1508
+ * Narrow gate for branding a nested `jsx-children` getter's value with
1509
+ * `bfMarkup()` (#2651) — the `irNodeToJsExprs` counterpart of
1510
+ * `collect-elements.ts`'s `isSingleElementJsxChildren` (duplicated rather
1511
+ * than imported: `collect-elements.ts` already imports FROM this file, so
1512
+ * the reverse import would cycle). Same shape, same reasoning: a
1513
+ * `jsx-children` prop's value is always a single stored node
1514
+ * (`AttrValueOf.jsxChildren([...])`); only when that node is a lone
1515
+ * `'element'` does `irChildrenToJsExpr` reduce it to ONE HTML-string
1516
+ * template literal — the shape `bfMarkup` + `escapeTextOrNode` /
1517
+ * `escapeTextOrMarkup` have a proven contract for. A `'fragment'` (multiple
1518
+ * children) or `'conditional'` node reduces to an array literal or a
1519
+ * nested ternary instead — left unbranded here for the same reason as the
1520
+ * `collect-elements.ts` twin.
1521
+ */
1522
+ function isSingleElementJsxChildren(nodes: IRNode[]): boolean {
1523
+ return nodes.length === 1 && nodes[0].type === 'element'
1524
+ }
1525
+
1481
1526
  function irNodeToJsExprs(node: IRNode): string[] {
1482
1527
  switch (node.type) {
1483
1528
  case 'component': {
@@ -1488,8 +1533,19 @@ function irNodeToJsExprs(node: IRNode): string[] {
1488
1533
  return `${quotePropName(p.name)}: ${attrValueToString(p.value) ?? 'undefined'}`
1489
1534
  }
1490
1535
  switch (p.value.kind) {
1491
- case 'jsx-children':
1492
- return `get ${quotePropName(p.name)}() { return ${irChildrenToJsExpr(p.value.children)} }`
1536
+ case 'jsx-children': {
1537
+ // Brand (#2651) only the lone-'element' shape, and never an
1538
+ // explicit `children={<jsx/>}` prop — see
1539
+ // `isSingleElementJsxChildren` (this file) for the shape
1540
+ // argument and the `collect-elements.ts` twin for the
1541
+ // `children`-exclusion argument (bare-passthrough consumer,
1542
+ // no unwrap call).
1543
+ const jsxExpr = irChildrenToJsExpr(p.value.children)
1544
+ const wrapped = (p.name !== 'children' && isSingleElementJsxChildren(p.value.children))
1545
+ ? `bfMarkup(${jsxExpr})`
1546
+ : jsxExpr
1547
+ return `get ${quotePropName(p.name)}() { return ${wrapped} }`
1548
+ }
1493
1549
  case 'literal':
1494
1550
  return `get ${quotePropName(p.name)}() { return ${JSON.stringify(p.value.value)} }`
1495
1551
  case 'boolean-shorthand':
@@ -1644,6 +1700,20 @@ export interface TemplateOptions {
1644
1700
  * the template phase agree on which children defer (dropped-prop fix).
1645
1701
  */
1646
1702
  deferredChildSlots?: ReadonlySet<string>
1703
+ /**
1704
+ * Slot ids of top-level, non-conditional dynamic expressions
1705
+ * (`ctx.dynamicElements`, populated by `collectElements` — the exact set
1706
+ * `emit-reactive.ts`'s `emitDynamicTextUpdates` claims `kind: 'markup'`
1707
+ * for on the REACTIVE side, #2651). When a text-marker expression's
1708
+ * `slotId` is a member, `escapeTextSlotExpr` emits `escapeTextOrMarkup`
1709
+ * instead of `escapeText` so a `bfMarkup()`-branded prop value (a JSX
1710
+ * element passed at a non-`children` component prop position) reaches
1711
+ * the initial-render template raw, matching what the reactive writer
1712
+ * already does via `escapeTextOrNode`. Computed once per component from
1713
+ * `ctx.dynamicElements` by `irToComponentTemplate` / `generateCsrTemplate`
1714
+ * — never re-derived here.
1715
+ */
1716
+ markupSlotIds?: ReadonlySet<string>
1647
1717
  }
1648
1718
 
1649
1719
  /**
@@ -1659,9 +1729,10 @@ export function irToComponentTemplate(
1659
1729
  node: IRNode,
1660
1730
  inlinableConstants?: Map<string, string>,
1661
1731
  restSpreadNames?: Set<string>,
1662
- propsObjectName?: string | null
1732
+ propsObjectName?: string | null,
1733
+ markupSlotIds?: ReadonlySet<string>
1663
1734
  ): string {
1664
- return irToComponentTemplateWithOpts(node, { inlinableConstants, restSpreadNames, propsObjectName, loopDepth: -1 })
1735
+ return irToComponentTemplateWithOpts(node, { inlinableConstants, restSpreadNames, propsObjectName, loopDepth: -1, markupSlotIds })
1665
1736
  }
1666
1737
 
1667
1738
  function irToComponentTemplateWithOpts(node: IRNode, opts: TemplateOptions): string {
@@ -1829,7 +1900,8 @@ function irToComponentTemplateWithOpts(node: IRNode, opts: TemplateOptions): str
1829
1900
  ? `Array.isArray(${wrapped}) ? ${wrapped}.join('') : (${wrapped} ?? '')`
1830
1901
  : wrapped
1831
1902
  if (node.slotId) {
1832
- return `<!--bf:${node.slotId}-->\${${node.joinArrayChild ? value : escapeTextSlotExpr(wrapped)}}<!--/-->`
1903
+ const isMarkup = opts.markupSlotIds?.has(node.slotId) ?? false
1904
+ return `<!--bf:${node.slotId}-->\${${node.joinArrayChild ? value : escapeTextSlotExpr(wrapped, isMarkup)}}<!--/-->`
1833
1905
  }
1834
1906
  return `\${${value}}`
1835
1907
  }
@@ -1873,7 +1945,12 @@ function irToComponentTemplateWithOpts(node: IRNode, opts: TemplateOptions): str
1873
1945
  case 'jsx-children': {
1874
1946
  const hoistedRecurse = (n: IRNode): string => irToComponentTemplateWithOpts(n, { ...opts, inHoistedChildren: true })
1875
1947
  const childHtml = p.value.children.map(c => hoistedRecurse(c)).join('')
1876
- return `${quotePropName(p.name)}: \`${childHtml}\``
1948
+ // Brand the assembled HTML (#2651), except an explicit
1949
+ // `children={<jsx/>}` prop — see the identical
1950
+ // `irToHtmlTemplate` case above for both arguments.
1951
+ return p.name === 'children'
1952
+ ? `${quotePropName(p.name)}: \`${childHtml}\``
1953
+ : `${quotePropName(p.name)}: bfMarkup(\`${childHtml}\`)`
1877
1954
  }
1878
1955
  case 'literal':
1879
1956
  return `${quotePropName(p.name)}: ${JSON.stringify(p.value.value)}`
@@ -2075,7 +2152,14 @@ export function generateCsrTemplate(
2075
2152
  }
2076
2153
  }
2077
2154
  const effectiveUnsafeLocalNames = mergeCsrNullUnsafe(ctx, unsafeLocalNames)
2078
- return generateCsrTemplateWithOpts(node, { inlinableConstants, restSpreadNames, propsObjectName, csrEnv, unsafeLocalNames: effectiveUnsafeLocalNames, deferredChildSlots, loopDepth: -1 })
2155
+ // `ctx.dynamicElements` (populated by `collectElements`, before any
2156
+ // template-string build runs) IS the claim-plan-'markup' membership
2157
+ // `emit-reactive.ts` claims for these same slot ids on the reactive
2158
+ // side (#2651) — every entry is already non-conditional / non-`@client`
2159
+ // by construction (`collectElements`'s `expression` visitor only pushes
2160
+ // here). Reused as-is, not re-derived.
2161
+ const markupSlotIds = new Set(ctx.dynamicElements.map(e => e.slotId))
2162
+ return generateCsrTemplateWithOpts(node, { inlinableConstants, restSpreadNames, propsObjectName, csrEnv, unsafeLocalNames: effectiveUnsafeLocalNames, deferredChildSlots, loopDepth: -1, markupSlotIds })
2079
2163
  }
2080
2164
 
2081
2165
  /**
@@ -2440,7 +2524,8 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
2440
2524
  ? `Array.isArray(${expr}) ? ${expr}.join('') : (${expr} ?? '')`
2441
2525
  : expr
2442
2526
  if (node.slotId) {
2443
- return `<!--bf:${node.slotId}-->\${${node.joinArrayChild ? value : escapeTextSlotExpr(expr)}}<!--/-->`
2527
+ const isMarkup = opts.markupSlotIds?.has(node.slotId) ?? false
2528
+ return `<!--bf:${node.slotId}-->\${${node.joinArrayChild ? value : escapeTextSlotExpr(expr, isMarkup)}}<!--/-->`
2444
2529
  }
2445
2530
  return `\${${value}}`
2446
2531
  }
@@ -2499,7 +2584,13 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
2499
2584
  case 'jsx-children': {
2500
2585
  const hoistedRecurse = (n: IRNode): string => generateCsrTemplateWithOpts(n, { ...opts, inHoistedChildren: true })
2501
2586
  const childHtml = p.value.children.map(c => hoistedRecurse(c)).join('')
2502
- return `${quotePropName(p.name)}: \`${childHtml}\``
2587
+ // Brand the assembled HTML (#2651), except an explicit
2588
+ // `children={<jsx/>}` prop — see the identical
2589
+ // `irToHtmlTemplate` case (top of this file) for both
2590
+ // arguments.
2591
+ return p.name === 'children'
2592
+ ? `${quotePropName(p.name)}: \`${childHtml}\``
2593
+ : `${quotePropName(p.name)}: bfMarkup(\`${childHtml}\`)`
2503
2594
  }
2504
2595
  case 'literal':
2505
2596
  return `${quotePropName(p.name)}: ${JSON.stringify(p.value.value)}`
@@ -20,6 +20,10 @@ export const RUNTIME_IMPORT_CANDIDATES = [
20
20
  'createPortal',
21
21
  'provideContext', 'createContext', 'useContext',
22
22
  'forwardProps', 'applyRestAttrs', 'splitProps', 'spreadAttrs', 'styleToCss', 'escapeAttr', 'escapeText', 'escapeTextOrNode',
23
+ // JSX-element-as-non-children-prop markup brand (#2651) — `bfMarkup` wraps
24
+ // the compiler-built HTML at the producer (renderChild / initChild props);
25
+ // `escapeTextOrMarkup` unwraps it at the claim-plan-'markup' template slot.
26
+ 'bfMarkup', 'escapeTextOrMarkup',
23
27
  'qsa', 'qsaItem', 'qsaChildScope', 'qsaChildScopes', 'upsertChildItem', '__slot', '__bfSlot', '__bfText',
24
28
  // Claim-plan interpreter (slot unification A2/A3, spec/slot-unification.md)
25
29
  // — the "one claim mechanism" that replaced `patchSlotRange` and
@@ -259,7 +259,12 @@ function generateTemplateOnlyMount(ir: ComponentIR, ctx: ClientJsContext): strin
259
259
  let templateHtml: string | undefined
260
260
 
261
261
  if (canGenerateStaticTemplate(ir.root, propNamesForStaticCheck, inlinableConstants, unsafeLocalNames)) {
262
- templateHtml = irToComponentTemplate(ir.root, inlinableConstants, restSpreadNames, ctx.propsObjectName)
262
+ // `ctx.dynamicElements` is always empty on this template-only path
263
+ // (`needsClientJs(ctx)` gates it) — passed through anyway so this
264
+ // matches `emitRegistrationAndHydration`'s derivation byte-for-byte
265
+ // rather than special-casing "no markup slots here" (#2651).
266
+ const markupSlotIds = new Set(ctx.dynamicElements.map(e => e.slotId))
267
+ templateHtml = irToComponentTemplate(ir.root, inlinableConstants, restSpreadNames, ctx.propsObjectName, markupSlotIds)
263
268
  }
264
269
 
265
270
  // CSR fallback: when static template generation fails (e.g., components with