@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.
- package/dist/index.js +127 -57
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts +66 -38
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-registration.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +15 -1
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts +2 -2
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +210 -210
- package/src/__tests__/aliased-destructured-prop-csr.test.ts +8 -2
- package/src/__tests__/csr-template-loop-shadowing.test.ts +5 -2
- package/src/__tests__/env-signal-template-prelude.test.ts +4 -2
- package/src/__tests__/inner-loop-mathml-namespace.test.ts +135 -0
- package/src/__tests__/markup-prop-brand.test.ts +161 -0
- package/src/__tests__/mathml-mapArray-namespace.test.ts +230 -0
- package/src/__tests__/text-slot-escaping.test.ts +21 -12
- package/src/ir-to-client-js/collect-elements.ts +38 -5
- package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +10 -10
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +3 -4
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +18 -14
- package/src/ir-to-client-js/control-flow/stringify/template-parse.ts +142 -78
- package/src/ir-to-client-js/emit-registration.ts +5 -1
- package/src/ir-to-client-js/html-template.ts +103 -12
- package/src/ir-to-client-js/imports.ts +4 -0
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|