@barefootjs/jsx 0.31.7 → 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 +136 -59
- 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/csr-substitute.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 +89 -0
- 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/csr-substitute.ts +3 -2
- package/src/ir-to-client-js/emit-registration.ts +56 -4
- 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
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* and the final hydrate() call emission.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { ComponentIR, IRFragment, IRNode, ReferencesGraph } from '../types.ts'
|
|
7
|
+
import type { ComponentIR, IRFragment, IRNode, ReferencesGraph, SignalInfo } from '../types.ts'
|
|
8
8
|
import type { ClientJsContext } from './types.ts'
|
|
9
9
|
import { PROPS_PARAM } from './utils.ts'
|
|
10
10
|
import { computeInlinability, toLegacyInlinability } from './compute-inlinability.ts'
|
|
@@ -115,6 +115,54 @@ export function csrInlinableConstantsFromCtx(ctx: ClientJsContext): Map<string,
|
|
|
115
115
|
return out
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Build the `template:` ComponentDef entry text for a generated
|
|
120
|
+
* `templateHtml` string.
|
|
121
|
+
*
|
|
122
|
+
* When the component holds one or more env signals (`createSearchParams()`,
|
|
123
|
+
* #2057), the template lambda destructures its own copy of each getter in
|
|
124
|
+
* a block-body prelude before returning the template literal:
|
|
125
|
+
*
|
|
126
|
+
* template: (_p) => { const [sp] = createSearchParams(); return `...` }
|
|
127
|
+
*
|
|
128
|
+
* instead of the plain expression-body form:
|
|
129
|
+
*
|
|
130
|
+
* template: (_p) => `...`
|
|
131
|
+
*
|
|
132
|
+
* The template lambda runs at module scope (`render()` / `renderChild()`),
|
|
133
|
+
* but an env-signal getter is otherwise only ever destructured inside
|
|
134
|
+
* `init...` — so a template that calls the getter directly (`sp()`,
|
|
135
|
+
* `searchParams()`) ReferenceErrors the moment it runs (#2654). Before
|
|
136
|
+
* #2057, `searchParams` was a bare module-scope import and the same
|
|
137
|
+
* template-body call worked by accident; #2057 moved it behind
|
|
138
|
+
* `const [sp] = createSearchParams()` without updating template emission.
|
|
139
|
+
*
|
|
140
|
+
* The prelude is emitted whenever the component HAS an env signal —
|
|
141
|
+
* never gated on whether `templateHtml` textually mentions the getter.
|
|
142
|
+
* Scanning already-emitted template HTML for a getter name would be a
|
|
143
|
+
* string/regex parse of emitted JS, which CLAUDE.md's parse rule forbids.
|
|
144
|
+
* Unconditional emission is safe because `createSearchParams()`
|
|
145
|
+
* (`packages/client/src/reactive.ts`) only returns the shared
|
|
146
|
+
* `searchParamsTuple` module singleton — no side effect — so declaring it
|
|
147
|
+
* in a prelude the template body doesn't end up using costs nothing.
|
|
148
|
+
*
|
|
149
|
+
* `envFactory` is expected to always be set alongside `envReader` (the
|
|
150
|
+
* analyzer sets both together, #2057) — the `undefined` branch is a
|
|
151
|
+
* defensive fallback: if it's ever missing, skip that signal's prelude
|
|
152
|
+
* line entirely rather than guessing a canonical factory name, leaving
|
|
153
|
+
* that one signal's template reference exactly as unsound as before this
|
|
154
|
+
* fix (never worse).
|
|
155
|
+
*/
|
|
156
|
+
function buildTemplateDefPart(ctx: ClientJsContext, templateHtml: string): string {
|
|
157
|
+
const envDecls = ctx.signals
|
|
158
|
+
.filter((s): s is SignalInfo & { envFactory: string } => Boolean(s.envReader) && Boolean(s.envFactory))
|
|
159
|
+
.map((s) => `const [${s.getter}] = ${s.envFactory}()`)
|
|
160
|
+
if (envDecls.length === 0) {
|
|
161
|
+
return `template: (${PROPS_PARAM}) => \`${templateHtml}\``
|
|
162
|
+
}
|
|
163
|
+
return `template: (${PROPS_PARAM}) => { ${envDecls.join('; ')}; return \`${templateHtml}\` }`
|
|
164
|
+
}
|
|
165
|
+
|
|
118
166
|
/** Emit hydrate() call that registers component, template, and hydrates. */
|
|
119
167
|
/**
|
|
120
168
|
* Generate the closing brace for the init function and the hydrate() call.
|
|
@@ -155,9 +203,13 @@ export function emitRegistrationAndHydration(
|
|
|
155
203
|
// Build ComponentDef object for hydrate()
|
|
156
204
|
const defParts: string[] = [`init: init${name}`]
|
|
157
205
|
if (canGenerateStaticTemplate(_ir.root, propNamesForStaticCheck, inlinableConstants, unsafeLocalNames)) {
|
|
158
|
-
|
|
206
|
+
// `ctx.dynamicElements` is the claim-plan-'markup' membership
|
|
207
|
+
// (#2651) — see `generateCsrTemplate`'s identical derivation below
|
|
208
|
+
// for why this is reused as-is rather than re-derived.
|
|
209
|
+
const markupSlotIds = new Set(ctx.dynamicElements.map(e => e.slotId))
|
|
210
|
+
const templateHtml = irToComponentTemplate(_ir.root, inlinableConstants, restSpreadNames, ctx.propsObjectName, markupSlotIds)
|
|
159
211
|
if (templateHtml) {
|
|
160
|
-
defParts.push(
|
|
212
|
+
defParts.push(buildTemplateDefPart(ctx, templateHtml))
|
|
161
213
|
}
|
|
162
214
|
} else {
|
|
163
215
|
// CSR fallback: emit for all components that can't generate static templates.
|
|
@@ -173,7 +225,7 @@ export function emitRegistrationAndHydration(
|
|
|
173
225
|
_ir.root, csrInlinableConstants, ctx, restSpreadNames, ctx.propsObjectName, unsafeLocalNames, ctx.deferredChildSlots
|
|
174
226
|
)
|
|
175
227
|
if (templateHtml) {
|
|
176
|
-
defParts.push(
|
|
228
|
+
defParts.push(buildTemplateDefPart(ctx, templateHtml))
|
|
177
229
|
}
|
|
178
230
|
}
|
|
179
231
|
// No else: top-level-only components skip template entirely (save bytes)
|
|
@@ -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
|