@barefootjs/jsx 0.34.0 → 0.35.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.
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +344 -215
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-component-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-composite-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts +11 -0
- package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-plain-row.d.ts +52 -0
- package/dist/ir-to-client-js/control-flow/plan/build-plain-row.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts +2 -0
- package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/lazy-conditional.d.ts +6 -7
- package/dist/ir-to-client-js/control-flow/plan/lazy-conditional.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/lazy-preamble.d.ts +5 -7
- package/dist/ir-to-client-js/control-flow/plan/lazy-preamble.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +10 -6
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/shared.d.ts +11 -9
- package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/composite-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts.map +1 -1
- package/dist/ir-to-client-js/csr-substitute.d.ts +16 -0
- package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.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 +2 -2
- 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/dist/ir-to-client-js/index.d.ts.map +1 -1
- package/dist/ir-to-client-js/markup-slots.d.ts +21 -0
- package/dist/ir-to-client-js/markup-slots.d.ts.map +1 -0
- package/dist/ir-to-client-js/safe-html.d.ts +158 -0
- package/dist/ir-to-client-js/safe-html.d.ts.map +1 -0
- package/dist/ir-to-client-js/utils.d.ts +34 -1
- package/dist/ir-to-client-js/utils.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/prop-rewrite.d.ts +8 -3
- package/dist/prop-rewrite.d.ts.map +1 -1
- package/dist/props-binding.d.ts +22 -1
- package/dist/props-binding.d.ts.map +1 -1
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/types.d.ts +5 -10
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +10 -12
- package/src/__tests__/build-plain-row-consolidation.test.ts +120 -0
- package/src/__tests__/client-template-escape-soundness.test.ts +538 -0
- package/src/__tests__/csr-template-loop-shadowing.test.ts +4 -2
- package/src/__tests__/flatmap-segments.test.ts +1 -1
- package/src/__tests__/lazy-conditional.test.ts +52 -12
- package/src/__tests__/lazy-preamble.test.ts +11 -11
- package/src/__tests__/lazy-row-eligibility.test.ts +8 -3
- package/src/__tests__/loop-child-reactive-attr-const-shadow.test.ts +25 -3
- package/src/__tests__/loop-item-conditional-codegen.test.ts +4 -2
- package/src/__tests__/map-arbitrary-body.test.ts +1 -1
- package/src/__tests__/nested-loop-index-param.test.ts +14 -3
- package/src/__tests__/nested-loop-reactive-attrs.test.ts +4 -2
- package/src/__tests__/reactive-attrs-in-map.test.ts +13 -10
- package/src/__tests__/rewrite-destructured-props.test.ts +37 -0
- package/src/__tests__/safe-html-splice-door.test.ts +61 -0
- package/src/__tests__/ssr-defaults.test.ts +59 -0
- package/src/analyzer.ts +4 -0
- package/src/index.ts +8 -2
- package/src/ir-to-client-js/collect-elements.ts +18 -2
- package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +20 -28
- package/src/ir-to-client-js/control-flow/plan/build-component-loop.ts +9 -7
- package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +10 -6
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +8 -8
- package/src/ir-to-client-js/control-flow/plan/build-lazy-row.ts +18 -2
- package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +3 -3
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +35 -35
- package/src/ir-to-client-js/control-flow/plan/build-plain-row.ts +110 -0
- package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +5 -2
- package/src/ir-to-client-js/control-flow/plan/lazy-conditional.ts +11 -10
- package/src/ir-to-client-js/control-flow/plan/lazy-preamble.ts +11 -13
- package/src/ir-to-client-js/control-flow/plan/lazy-row-eligibility.ts +13 -3
- package/src/ir-to-client-js/control-flow/plan/loop.ts +10 -6
- package/src/ir-to-client-js/control-flow/shared.ts +40 -17
- package/src/ir-to-client-js/control-flow/stringify/composite-loop.ts +2 -1
- package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +10 -0
- package/src/ir-to-client-js/csr-substitute.ts +29 -0
- package/src/ir-to-client-js/emit-reactive.ts +4 -1
- package/src/ir-to-client-js/emit-registration.ts +2 -4
- package/src/ir-to-client-js/html-template.ts +58 -244
- package/src/ir-to-client-js/imports.ts +3 -0
- package/src/ir-to-client-js/index.ts +4 -4
- package/src/ir-to-client-js/markup-slots.ts +25 -0
- package/src/ir-to-client-js/safe-html.ts +243 -0
- package/src/ir-to-client-js/utils.ts +58 -10
- package/src/jsx-to-ir.ts +115 -20
- package/src/prop-rewrite.ts +8 -4
- package/src/props-binding.ts +37 -1
- package/src/ssr-defaults.ts +45 -0
- package/src/types.ts +5 -10
- package/src/__tests__/text-slot-escaping.test.ts +0 -65
|
@@ -15,6 +15,8 @@ import { BF_PARENT_SCOPE_PLACEHOLDER, BF_SCOPE, classifyDOMProp, escapeHtml } fr
|
|
|
15
15
|
import { buildLoopChainExpr } from '../loop-chain.ts'
|
|
16
16
|
import { renderChildScopeArgs } from '../adapters/child-scope.ts'
|
|
17
17
|
import { BindingScope } from '../scope/binding-scope.ts'
|
|
18
|
+
import { interp, spliceChildValue, conditionalMarkup, renderChildCall, mappedRowsMarkup, dangerousInnerHtml, EMPTY_MARKUP } from './safe-html.ts'
|
|
19
|
+
import { markupSlotIdsOf } from './markup-slots.ts'
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* Protect string literals from regex-based replacements.
|
|
@@ -310,113 +312,6 @@ function escapeAttrValueExpr(valExpr: string): string {
|
|
|
310
312
|
return `escapeAttr(${valExpr})`
|
|
311
313
|
}
|
|
312
314
|
|
|
313
|
-
/**
|
|
314
|
-
* Build a runtime expression that HTML-escapes an interpolated **text
|
|
315
|
-
* content** slot, via the `escapeText` runtime helper. Only the
|
|
316
|
-
* `<!--bf:sN-->${expr}<!--/-->` text-marker form is text content: the
|
|
317
|
-
* runtime treats whatever sits between the markers as the slot's text, so
|
|
318
|
-
* a string value containing `<` / `&` (e.g. `{user.name}`) must be escaped
|
|
319
|
-
* to parse correctly under `innerHTML` and to match the SSR-rendered
|
|
320
|
-
* bytes. Bare `${...}` interpolations — `{children}` passthrough and
|
|
321
|
-
* `renderChild(...)` output — are pre-rendered HTML and must NOT be
|
|
322
|
-
* escaped, so this is applied only at the four text-marker emit sites.
|
|
323
|
-
* The no-`slotId` fallthrough (every `case 'expression'` branch's final
|
|
324
|
-
* `return` in this file) is shared by several unrelated shapes besides
|
|
325
|
-
* `{children}` passthrough — an `escapeLeafTextExpressions`-wrapped
|
|
326
|
-
* preamble leaf, `lowerFormControlValueSsr`'s textarea initial value, an
|
|
327
|
-
* inlined constant, a `''`/`undefined` deferred placeholder — every one of
|
|
328
|
-
* which is either already escaped or a literal, and must reach the
|
|
329
|
-
* template untouched. `bareSpliceExpr` below is that branch's single door
|
|
330
|
-
* — the one place the fallthrough's decision is made — and it is what
|
|
331
|
-
* picks the genuine `{children}` reference back out for `markupOrEmpty`'s
|
|
332
|
-
* nullish guard (#2775); see its own docstring.
|
|
333
|
-
* Hono escapes text content with the same set as attribute values
|
|
334
|
-
* (`& " ' < >`), so `escapeText` delegates to the same operation.
|
|
335
|
-
*
|
|
336
|
-
* `isMarkup` (#2651) switches to `escapeTextOrMarkup` — `escapeText`'s
|
|
337
|
-
* strict superset that additionally unwraps a `bfMarkup()`-branded value
|
|
338
|
-
* raw instead of escaping it. Two of the four text-marker call sites
|
|
339
|
-
* (`irToComponentTemplateWithOpts`, `generateCsrTemplateWithOpts`) pass
|
|
340
|
-
* `true` for a slot `ctx.dynamicElements` also claims — the same
|
|
341
|
-
* membership `emit-reactive.ts` uses to pick the 'markup' writer kind for
|
|
342
|
-
* this slot's REACTIVE update, so the initial-render escape and the
|
|
343
|
-
* reactive-update escape agree on whether the slot may carry raw markup.
|
|
344
|
-
* The other two (`irToHtmlTemplate`, `irToPlaceholderTemplate`) build
|
|
345
|
-
* loop-item / conditional-branch HTML, which the reactive side always
|
|
346
|
-
* treats as plain text (`__bfText`) regardless — they never pass `true`,
|
|
347
|
-
* so they keep calling `escapeText` byte-for-byte as before.
|
|
348
|
-
*/
|
|
349
|
-
function escapeTextSlotExpr(innerExpr: string, isMarkup = false): string {
|
|
350
|
-
return `${isMarkup ? 'escapeTextOrMarkup' : 'escapeText'}(${innerExpr})`
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* Recognizes a JSX child-position expression that is exactly a reference to
|
|
355
|
-
* the reserved `children` prop — bare `children` (destructured) or
|
|
356
|
-
* `<receiver>.children` for any single-identifier receiver (`props.children`,
|
|
357
|
-
* a custom props-param name, a loop-scoped alias closing over props, ...).
|
|
358
|
-
* Checked against `node.expr` — the ORIGINAL source text, never a
|
|
359
|
-
* transformed/wrapped form — so it stays accurate regardless of which
|
|
360
|
-
* builder is asking, and regardless of any earlier pass
|
|
361
|
-
* (`escapeLeafTextExpressions`, `lowerFormControlValueSsr`) that may have
|
|
362
|
-
* wrapped an unrelated leaf.
|
|
363
|
-
*
|
|
364
|
-
* Deliberately LOOSER than `isTransparentFragment` (`jsx-to-ir.ts`), which
|
|
365
|
-
* answers the same underlying question one level up. That function runs on
|
|
366
|
-
* the TS AST and compares the expression text against an EXACT set —
|
|
367
|
-
* `children`, `props.children`, and the analyzer-resolved
|
|
368
|
-
* `${ctx.analyzer.propsObjectName}.children`. This layer works on IR and has
|
|
369
|
-
* no analyzer, so the resolved props name is not reachable here; matching any
|
|
370
|
-
* single-identifier receiver is the available approximation, chosen — not an
|
|
371
|
-
* inherited convention.
|
|
372
|
-
*
|
|
373
|
-
* The looseness costs nothing measurable. An unrelated `.children` member —
|
|
374
|
-
* a tree node's own `children` array, say — does not even arrive here: a
|
|
375
|
-
* reactive member expression is given a `slotId` and takes the escaped
|
|
376
|
-
* text-slot branch above, so it never reaches the bare-splice fallthrough
|
|
377
|
-
* this gates. And were one to arrive, the outcome is still benign: the
|
|
378
|
-
* branch never escaped its value either way, a non-nullish value is
|
|
379
|
-
* returned untouched, and a nullish one rendering `''` instead of the
|
|
380
|
-
* literal `"undefined"` is an improvement in its own right.
|
|
381
|
-
*
|
|
382
|
-
* Both the ORIGINAL source text and the RESOLVED expression are tested,
|
|
383
|
-
* because either one alone misses a shape. `node.expr` is the only form
|
|
384
|
-
* that does not vary between the four builders, so it stays the primary
|
|
385
|
-
* test; but it is the pre-substitution text, which for a
|
|
386
|
-
* destructured-and-renamed children (`const { children: kids } = props`)
|
|
387
|
-
* reads `kids` and matches nothing — while the resolved expression the
|
|
388
|
-
* emitter is about to splice already reads `(_p.children)`. Testing both
|
|
389
|
-
* closes that (#2786) without giving up `node.expr`'s stability.
|
|
390
|
-
*/
|
|
391
|
-
function isChildrenPassthroughExpr(expr: string): boolean {
|
|
392
|
-
return /^([A-Za-z_$][\w$]*\.)?children$/.test(expr.trim())
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
* The single door for the bare (no-`slotId`) expression splice — the
|
|
397
|
-
* counterpart to `escapeTextSlotExpr` for the branch that must NOT escape.
|
|
398
|
-
* All four `case 'expression'` builders in this file route through here so
|
|
399
|
-
* this decision exists in exactly one place: four copies that agree today
|
|
400
|
-
* are four that can drift apart tomorrow, and this file is where that has
|
|
401
|
-
* already happened (#2753 -> #2762).
|
|
402
|
-
*
|
|
403
|
-
* Only a genuine `{children}` passthrough gets `markupOrEmpty`'s nullish
|
|
404
|
-
* guard (#2775). Everything else this fallthrough hosts — an
|
|
405
|
-
* `escapeLeafTextExpressions`-wrapped preamble leaf,
|
|
406
|
-
* `lowerFormControlValueSsr`'s textarea initial value, an inlined constant,
|
|
407
|
-
* a `''`/`undefined` deferred placeholder — reaches the template exactly as
|
|
408
|
-
* it arrived, already escaped or a literal. Escaping is never correct here:
|
|
409
|
-
* the value is pre-rendered HTML, per `escapeTextSlotExpr`'s docstring.
|
|
410
|
-
*/
|
|
411
|
-
function bareSpliceExpr(node: IRExpression, valueExpr: string): string {
|
|
412
|
-
// Strip the parens the emitter wraps a substituted expression in, so the
|
|
413
|
-
// resolved form is comparable to the bare source text.
|
|
414
|
-
const resolved = valueExpr.trim().replace(/^\(+|\)+$/g, '')
|
|
415
|
-
const isChildren =
|
|
416
|
-
isChildrenPassthroughExpr(node.expr) || isChildrenPassthroughExpr(resolved)
|
|
417
|
-
return !node.joinArrayChild && isChildren ? `markupOrEmpty(${valueExpr})` : valueExpr
|
|
418
|
-
}
|
|
419
|
-
|
|
420
315
|
/**
|
|
421
316
|
* `dangerouslySetInnerHTML={{ __html: E }}` makes the element's content its
|
|
422
317
|
* raw innerHTML — the intentional, React-style escape hatch. Returns the
|
|
@@ -435,7 +330,7 @@ function dangerouslyHtmlChildren(
|
|
|
435
330
|
): string | null {
|
|
436
331
|
const attr = attrs.find(a => a.name === 'dangerouslySetInnerHTML')
|
|
437
332
|
if (!attr || attr.value.kind !== 'expression') return null
|
|
438
|
-
return
|
|
333
|
+
return interp(dangerousInnerHtml(toExpr(attr.value)))
|
|
439
334
|
}
|
|
440
335
|
|
|
441
336
|
/**
|
|
@@ -672,9 +567,17 @@ function buildSpreadAttrsMergeCall(args: {
|
|
|
672
567
|
* expression (e.g. `t.id`). Emits a live `${keyExpr}` interpolation so each
|
|
673
568
|
* rendered item carries its own key — `loopItemMarker` is reserved for
|
|
674
569
|
* already-evaluated key strings (runtime / static contexts).
|
|
570
|
+
*
|
|
571
|
+
* The key is wrapped in `escapeCommentText` (#2795 follow-up): unlike every
|
|
572
|
+
* other hole in this file, this one lands inside HTML COMMENT content, where
|
|
573
|
+
* `escapeText`'s `& < > " '` escaping does nothing — the only thing that
|
|
574
|
+
* matters here is that the key can't spell `-->` and close the comment
|
|
575
|
+
* early. See `escapeCommentText`'s docstring (`@barefootjs/client/runtime`)
|
|
576
|
+
* for why a lossy hyphen substitution is fine: nothing reads this key back
|
|
577
|
+
* out of the DOM.
|
|
675
578
|
*/
|
|
676
579
|
function itemAnchorTemplate(keyExpr: string): string {
|
|
677
|
-
return `<!--${loopItemMarker('${' + keyExpr + '}')}-->`
|
|
580
|
+
return `<!--${loopItemMarker('${escapeCommentText(' + keyExpr + ')}')}-->`
|
|
678
581
|
}
|
|
679
582
|
|
|
680
583
|
/**
|
|
@@ -710,9 +613,9 @@ export function renderPreamble(
|
|
|
710
613
|
const text = opts.textVariant === 'template' ? (seg.templateText ?? seg.text) : seg.text
|
|
711
614
|
out += opts.transformJs ? opts.transformJs(text) : text
|
|
712
615
|
} else if (opts.rawLeaf) {
|
|
713
|
-
out += opts.renderLeaf(
|
|
616
|
+
out += opts.renderLeaf(seg.ir)
|
|
714
617
|
} else {
|
|
715
|
-
out += '`' + opts.renderLeaf(
|
|
618
|
+
out += '`' + opts.renderLeaf(seg.ir) + '`'
|
|
716
619
|
}
|
|
717
620
|
}
|
|
718
621
|
return out
|
|
@@ -803,43 +706,11 @@ export function renderFlatMapProjectionClientBody(
|
|
|
803
706
|
const params = inner.index ? `(${inner.param}, ${inner.index})` : `(${inner.param})`
|
|
804
707
|
const key = inner.key ? `(${inner.key})` : 'undefined'
|
|
805
708
|
const html = inner.children
|
|
806
|
-
.map((c) => irToHtmlTemplate(
|
|
709
|
+
.map((c) => irToHtmlTemplate(c, restSpreadNames, 1, undefined, undefined))
|
|
807
710
|
.join('')
|
|
808
711
|
return `${chained}.map(${params} => ({ k: ${key}, h: \`${html}\` }))`
|
|
809
712
|
}
|
|
810
713
|
|
|
811
|
-
/**
|
|
812
|
-
* SSR/CSR escaping parity for preamble leaves, decided once at the door: a
|
|
813
|
-
* JSX-runtime SSR adapter renders the leaf's raw JSX and auto-escapes text
|
|
814
|
-
* interpolations (`{c}`), so the client's HTML-string lowering must escape the
|
|
815
|
-
* same positions — wrap every text-position expression in `escapeText(...)`.
|
|
816
|
-
* Pure (returns a transformed copy); the neutral IR is never mutated with a
|
|
817
|
-
* client-only concern. Attribute values already flow through the template
|
|
818
|
-
* emitters' own attr escaping.
|
|
819
|
-
*/
|
|
820
|
-
function escapeLeafTextExpressions(ir: IRNode): IRNode {
|
|
821
|
-
switch (ir.type) {
|
|
822
|
-
case 'element':
|
|
823
|
-
return { ...ir, children: ir.children.map(escapeLeafTextExpressions) }
|
|
824
|
-
case 'fragment':
|
|
825
|
-
return { ...ir, children: ir.children.map(escapeLeafTextExpressions) }
|
|
826
|
-
case 'expression': {
|
|
827
|
-
if (ir.expr === 'null' || ir.expr === 'undefined') return ir
|
|
828
|
-
// Already-wrapped or slotted expressions keep their existing handling.
|
|
829
|
-
if (ir.slotId || ir.expr.trimStart().startsWith('escapeText(')) return ir
|
|
830
|
-
return { ...ir, expr: `escapeText((${ir.expr}))`, templateExpr: ir.templateExpr ? `escapeText((${ir.templateExpr}))` : ir.templateExpr }
|
|
831
|
-
}
|
|
832
|
-
case 'conditional':
|
|
833
|
-
return {
|
|
834
|
-
...ir,
|
|
835
|
-
whenTrue: escapeLeafTextExpressions(ir.whenTrue),
|
|
836
|
-
whenFalse: ir.whenFalse ? escapeLeafTextExpressions(ir.whenFalse) : ir.whenFalse,
|
|
837
|
-
}
|
|
838
|
-
default:
|
|
839
|
-
return ir
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
|
|
843
714
|
// `loopParams` here is the accessor-rewrite spec list `wrapExprWithLoopParams`
|
|
844
715
|
// consumes, not a `BindingScope`-trackable name set — see that function's
|
|
845
716
|
// docstring (`ir-to-client-js/utils.ts`) for why this stays outside #2482's
|
|
@@ -848,9 +719,6 @@ function escapeLeafTextExpressions(ir: IRNode): IRNode {
|
|
|
848
719
|
export function irToHtmlTemplate(node: IRNode, restSpreadNames?: ReadonlySet<string>, loopDepth = 0, loopParams?: ReadonlyArray<string | LoopParamSpec>, branchSlotsVar?: string, inHoistedChildren = false): string {
|
|
849
720
|
const recurse = (n: IRNode): string => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar, inHoistedChildren)
|
|
850
721
|
const wrapExpr = (expr: string) => wrapExprWithLoopParams(expr, loopParams)
|
|
851
|
-
const wrapInterpolation = (expr: string): string => branchSlotsVar
|
|
852
|
-
? `__bfSlot(${expr}, ${branchSlotsVar})`
|
|
853
|
-
: expr
|
|
854
722
|
|
|
855
723
|
switch (node.type) {
|
|
856
724
|
case 'element': {
|
|
@@ -967,38 +835,13 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: ReadonlySet<str
|
|
|
967
835
|
// because their `clientOnly` semantics differ — see that function's
|
|
968
836
|
// own comment (#2617).
|
|
969
837
|
|
|
970
|
-
//
|
|
971
|
-
//
|
|
972
|
-
//
|
|
973
|
-
// the
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
if (node.markerless) {
|
|
978
|
-
const bare = escapeForClient(wrapInterpolation(wrapExpr(node.expr)))
|
|
979
|
-
return `\${${bare}}`
|
|
980
|
-
}
|
|
981
|
-
const inner = escapeForClient(wrapInterpolation(wrapExpr(node.expr)))
|
|
982
|
-
// Stage 3 / D4 — an element-array child ({out}) built by an arbitrary
|
|
983
|
-
// .map() preamble is an array of HTML strings; join it rather than let
|
|
984
|
-
// `${[...]}` `String`-comma-collapse it. Only reached on a JS-runtime
|
|
985
|
-
// adapter (the flag is set in Phase 1 only there); a plain local can be
|
|
986
|
-
// read twice safely.
|
|
987
|
-
const valueExpr = node.joinArrayChild
|
|
988
|
-
? `Array.isArray(${inner}) ? ${inner}.join('') : (${inner} ?? '')`
|
|
989
|
-
: inner
|
|
990
|
-
if (node.slotId) {
|
|
991
|
-
// In branch-slot context `wrapInterpolation` routes the value
|
|
992
|
-
// through `__bfSlot`, which returns raw `<!--bf-slot:N-->` markers
|
|
993
|
-
// for live `Node` values (spliced back by `insert()`). Escaping
|
|
994
|
-
// would corrupt those markers and drop slotted content (#1694
|
|
995
|
-
// regression). `__bfSlot` owns coercion of its own value, so the
|
|
996
|
-
// text-escape applies only to the non-slot (plain text) form.
|
|
997
|
-
const slotted = branchSlotsVar || node.joinArrayChild ? valueExpr : escapeTextSlotExpr(valueExpr)
|
|
998
|
-
return `<!--bf:${node.slotId}-->\${${slotted}}<!--/-->`
|
|
999
|
-
}
|
|
1000
|
-
// Bare-splice fallthrough (no `slotId`, not an array-child join).
|
|
1001
|
-
return `\${${bareSpliceExpr(node, valueExpr)}}`
|
|
838
|
+
// Never take `templateExpr` here instead of `expr`: it rebinds to
|
|
839
|
+
// `_p.xxx`, dropping the `?? {}` prop-defaulting guard in this
|
|
840
|
+
// builder's init scope (`client-js-generation.test.ts`). Escaping is
|
|
841
|
+
// the door's job (`spliceChildValue`), not this builder's.
|
|
842
|
+
const hole = interp(spliceChildValue(node, wrapExpr(node.expr), { branchSlotsVar }))
|
|
843
|
+
if (node.markerless) return hole
|
|
844
|
+
return node.slotId ? `<!--bf:${node.slotId}-->${hole}<!--/-->` : hole
|
|
1002
845
|
}
|
|
1003
846
|
|
|
1004
847
|
case 'conditional': {
|
|
@@ -1006,7 +849,7 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: ReadonlySet<str
|
|
|
1006
849
|
const falseBranch = recurse(node.whenFalse)
|
|
1007
850
|
const trueHtml = node.slotId ? addCondAttrToTemplate(trueBranch, node.slotId) : trueBranch
|
|
1008
851
|
const falseHtml = node.slotId ? addCondAttrToTemplate(falseBranch, node.slotId) : falseBranch
|
|
1009
|
-
return
|
|
852
|
+
return interp(conditionalMarkup(wrapExpr(node.condition), trueHtml, falseHtml))
|
|
1010
853
|
}
|
|
1011
854
|
|
|
1012
855
|
case 'fragment':
|
|
@@ -1044,8 +887,8 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: ReadonlySet<str
|
|
|
1044
887
|
// trust the brand and skip re-escaping it. EXCEPT an explicit
|
|
1045
888
|
// `children={<jsx/>}` prop (out of scope, unchanged): the
|
|
1046
889
|
// child's `{children}` interpolation is the bare-passthrough
|
|
1047
|
-
//
|
|
1048
|
-
// through an escape/unwrap call, so a branded object here
|
|
890
|
+
// arm of `spliceChildValue`'s door (`safe-html.ts`), never
|
|
891
|
+
// routed through an escape/unwrap call, so a branded object here
|
|
1049
892
|
// would stringify to `[object Object]` instead of unwrapping.
|
|
1050
893
|
return p.name === 'children'
|
|
1051
894
|
? `${quotePropName(p.name)}: \`${childHtml}\``
|
|
@@ -1085,7 +928,7 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: ReadonlySet<str
|
|
|
1085
928
|
// the slot — `renderChildScopeArgs` still passes its slotId with the
|
|
1086
929
|
// `loopItemRoot` flag (#2833) so it keeps `bf-h`/`bf-m` slot identity,
|
|
1087
930
|
// matching the Hono reference (#2444).
|
|
1088
|
-
return
|
|
931
|
+
return interp(renderChildCall(nameForRegistryRef(node.name), propsExpr, renderChildScopeArgs(node, keyArg)))
|
|
1089
932
|
}
|
|
1090
933
|
|
|
1091
934
|
case 'loop': {
|
|
@@ -1130,14 +973,14 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: ReadonlySet<str
|
|
|
1130
973
|
const body = renderPreamble(node.flatMapCallback, {
|
|
1131
974
|
renderLeaf: (ir) => irToHtmlTemplate(stripLeafKeyAttr(ir), restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar),
|
|
1132
975
|
})
|
|
1133
|
-
mapExpr =
|
|
976
|
+
mapExpr = interp(mappedRowsMarkup(wrappedArray, 'flatMap', node.flatMapCallback.params, body))
|
|
1134
977
|
} else if (node.preamble) {
|
|
1135
978
|
// Stage 3 / D4 — render JSX leaves in an arbitrary array-builder
|
|
1136
979
|
// preamble (the hydrate-template context uses the bare loop param).
|
|
1137
980
|
const preamble = renderPreamble(node.preamble, { textVariant: 'client', renderLeaf: (ir) => irToHtmlTemplate(ir, restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar) })
|
|
1138
|
-
mapExpr =
|
|
981
|
+
mapExpr = interp(mappedRowsMarkup(wrappedArray, iterMethod, callbackParam, `{ ${preamble} return \`${childTemplate}\` }`))
|
|
1139
982
|
} else {
|
|
1140
|
-
mapExpr =
|
|
983
|
+
mapExpr = interp(mappedRowsMarkup(wrappedArray, iterMethod, callbackParam, `\`${childTemplate}\``))
|
|
1141
984
|
}
|
|
1142
985
|
// Wrap with loop boundary markers so reconciliation doesn't affect siblings
|
|
1143
986
|
return `<!--${loopStartMarker(node.markerId)}-->${mapExpr}<!--${loopEndMarker(node.markerId)}-->`
|
|
@@ -1552,19 +1395,10 @@ export function irToPlaceholderTemplate(node: IRNode, restSpreadNames?: Readonly
|
|
|
1552
1395
|
|
|
1553
1396
|
case 'expression': {
|
|
1554
1397
|
if (node.expr === 'null' || node.expr === 'undefined') return ''
|
|
1555
|
-
|
|
1556
|
-
//
|
|
1557
|
-
const
|
|
1558
|
-
|
|
1559
|
-
: wrapped
|
|
1560
|
-
if (node.slotId) {
|
|
1561
|
-
return `<!--bf:${node.slotId}-->\${${node.joinArrayChild ? value : escapeTextSlotExpr(wrapped)}}<!--/-->`
|
|
1562
|
-
}
|
|
1563
|
-
// Bare-splice fallthrough (no `slotId`) — this builder's composite-row
|
|
1564
|
-
// twin of `irToHtmlTemplate`'s `escapeForClient`, same "why not
|
|
1565
|
-
// templateExpr" reasoning (#2765).
|
|
1566
|
-
const spliced = bareSpliceExpr(node, value)
|
|
1567
|
-
return `\${${node.escapeInClientTemplate ? `escapeText(${spliced})` : spliced}}`
|
|
1398
|
+
// Same "`expr`, never `templateExpr`" reasoning as `irToHtmlTemplate`
|
|
1399
|
+
// (#2765); escaping is the door's job.
|
|
1400
|
+
const hole = interp(spliceChildValue(node, wrapExpr(node.expr), {}))
|
|
1401
|
+
return node.slotId ? `<!--bf:${node.slotId}-->${hole}<!--/-->` : hole
|
|
1568
1402
|
}
|
|
1569
1403
|
|
|
1570
1404
|
case 'conditional': {
|
|
@@ -1572,7 +1406,7 @@ export function irToPlaceholderTemplate(node: IRNode, restSpreadNames?: Readonly
|
|
|
1572
1406
|
const falseBranch = recurse(node.whenFalse)
|
|
1573
1407
|
const trueHtml = node.slotId ? addCondAttrToTemplate(trueBranch, node.slotId) : trueBranch
|
|
1574
1408
|
const falseHtml = node.slotId ? addCondAttrToTemplate(falseBranch, node.slotId) : falseBranch
|
|
1575
|
-
return
|
|
1409
|
+
return interp(conditionalMarkup(wrapExpr(node.condition), trueHtml, falseHtml))
|
|
1576
1410
|
}
|
|
1577
1411
|
|
|
1578
1412
|
case 'fragment':
|
|
@@ -1606,13 +1440,13 @@ export function irToPlaceholderTemplate(node: IRNode, restSpreadNames?: Readonly
|
|
|
1606
1440
|
// Leaf `key` stripped — see the irToHtmlTemplate site above.
|
|
1607
1441
|
renderLeaf: (ir) => irToPlaceholderTemplate(stripLeafKeyAttr(ir), restSpreadNames, loopDepth + 1, loopParams),
|
|
1608
1442
|
})
|
|
1609
|
-
mapExpr =
|
|
1443
|
+
mapExpr = interp(mappedRowsMarkup(wrappedArray, 'flatMap', node.flatMapCallback.params, body))
|
|
1610
1444
|
} else if (node.preamble) {
|
|
1611
1445
|
// Stage 3 / D4 — render JSX leaves in an arbitrary array-builder preamble.
|
|
1612
1446
|
const preamble = renderPreamble(node.preamble, { textVariant: 'client', renderLeaf: (ir) => irToPlaceholderTemplate(ir, restSpreadNames, loopDepth + 1, loopParams) })
|
|
1613
|
-
mapExpr =
|
|
1447
|
+
mapExpr = interp(mappedRowsMarkup(wrappedArray, iterMethod, callbackParam, `{ ${preamble} return \`${childTemplate}\` }`))
|
|
1614
1448
|
} else {
|
|
1615
|
-
mapExpr =
|
|
1449
|
+
mapExpr = interp(mappedRowsMarkup(wrappedArray, iterMethod, callbackParam, `\`${childTemplate}\``))
|
|
1616
1450
|
}
|
|
1617
1451
|
return `<!--${loopStartMarker(node.markerId)}-->${mapExpr}<!--${loopEndMarker(node.markerId)}-->`
|
|
1618
1452
|
}
|
|
@@ -1881,8 +1715,8 @@ export interface TemplateOptions {
|
|
|
1881
1715
|
* (`ctx.dynamicElements`, populated by `collectElements` — the exact set
|
|
1882
1716
|
* `emit-reactive.ts`'s `emitDynamicTextUpdates` claims `kind: 'markup'`
|
|
1883
1717
|
* for on the REACTIVE side, #2651). When a text-marker expression's
|
|
1884
|
-
* `slotId` is a member, `
|
|
1885
|
-
* instead of `escapeText` so a `bfMarkup()`-branded prop value (a JSX
|
|
1718
|
+
* `slotId` is a member, `spliceChildValue`'s door (`safe-html.ts`) emits
|
|
1719
|
+
* `escapeTextOrMarkup` instead of `escapeText` so a `bfMarkup()`-branded prop value (a JSX
|
|
1886
1720
|
* element passed at a non-`children` component prop position) reaches
|
|
1887
1721
|
* the initial-render template raw, matching what the reactive writer
|
|
1888
1722
|
* already does via `escapeTextOrNode`. Computed once per component from
|
|
@@ -2087,17 +1921,8 @@ function irToComponentTemplateWithOpts(node: IRNode, opts: TemplateOptions): str
|
|
|
2087
1921
|
if (node.markerless) return ''
|
|
2088
1922
|
return `<!--bf:${node.slotId}--><!--/-->`
|
|
2089
1923
|
}
|
|
2090
|
-
const
|
|
2091
|
-
|
|
2092
|
-
const value = node.joinArrayChild
|
|
2093
|
-
? `Array.isArray(${wrapped}) ? ${wrapped}.join('') : (${wrapped} ?? '')`
|
|
2094
|
-
: wrapped
|
|
2095
|
-
if (node.slotId) {
|
|
2096
|
-
const isMarkup = opts.markupSlotIds?.has(node.slotId) ?? false
|
|
2097
|
-
return `<!--bf:${node.slotId}-->\${${node.joinArrayChild ? value : escapeTextSlotExpr(wrapped, isMarkup)}}<!--/-->`
|
|
2098
|
-
}
|
|
2099
|
-
// Bare-splice fallthrough (no `slotId`).
|
|
2100
|
-
return `\${${bareSpliceExpr(node, value)}}`
|
|
1924
|
+
const hole = interp(spliceChildValue(node, transformExpr(node.expr, node.templateExpr), { markupSlotIds: opts.markupSlotIds }))
|
|
1925
|
+
return node.slotId ? `<!--bf:${node.slotId}-->${hole}<!--/-->` : hole
|
|
2101
1926
|
}
|
|
2102
1927
|
|
|
2103
1928
|
case 'conditional': {
|
|
@@ -2113,7 +1938,7 @@ function irToComponentTemplateWithOpts(node: IRNode, opts: TemplateOptions): str
|
|
|
2113
1938
|
const falseBranch = recurse(node.whenFalse)
|
|
2114
1939
|
const trueHtml = node.slotId ? addCondAttrToTemplate(trueBranch, node.slotId) : trueBranch
|
|
2115
1940
|
const falseHtml = node.slotId ? addCondAttrToTemplate(falseBranch, node.slotId) : falseBranch
|
|
2116
|
-
return
|
|
1941
|
+
return interp(conditionalMarkup(transformExpr(node.condition, node.templateCondition), trueHtml, falseHtml))
|
|
2117
1942
|
}
|
|
2118
1943
|
|
|
2119
1944
|
case 'fragment':
|
|
@@ -2167,7 +1992,7 @@ function irToComponentTemplateWithOpts(node: IRNode, opts: TemplateOptions): str
|
|
|
2167
1992
|
const propsExpr = propsEntries.length > 0 ? `{${propsEntries.join(', ')}}` : '{}'
|
|
2168
1993
|
const keyProp = node.props.find(p => p.name === 'key')
|
|
2169
1994
|
const keyArg = keyProp ? `, ${transformKeyValue(keyProp.value, transformExpr)}` : ''
|
|
2170
|
-
return
|
|
1995
|
+
return interp(renderChildCall(nameForRegistryRef(node.name), propsExpr, keyArg))
|
|
2171
1996
|
}
|
|
2172
1997
|
|
|
2173
1998
|
case 'loop': {
|
|
@@ -2187,7 +2012,7 @@ function irToComponentTemplateWithOpts(node: IRNode, opts: TemplateOptions): str
|
|
|
2187
2012
|
// with the conditional-template path.
|
|
2188
2013
|
const consequent = recurse(node.consequent)
|
|
2189
2014
|
const alternate = node.alternate ? recurse(node.alternate) : ''
|
|
2190
|
-
return
|
|
2015
|
+
return interp(conditionalMarkup(transformExpr(node.condition, node.templateCondition), consequent, alternate))
|
|
2191
2016
|
}
|
|
2192
2017
|
|
|
2193
2018
|
case 'provider':
|
|
@@ -2349,13 +2174,7 @@ export function generateCsrTemplate(
|
|
|
2349
2174
|
}
|
|
2350
2175
|
}
|
|
2351
2176
|
const effectiveUnsafeLocalNames = mergeCsrNullUnsafe(ctx, unsafeLocalNames)
|
|
2352
|
-
|
|
2353
|
-
// template-string build runs) IS the claim-plan-'markup' membership
|
|
2354
|
-
// `emit-reactive.ts` claims for these same slot ids on the reactive
|
|
2355
|
-
// side (#2651) — every entry is already non-conditional / non-`@client`
|
|
2356
|
-
// by construction (`collectElements`'s `expression` visitor only pushes
|
|
2357
|
-
// here). Reused as-is, not re-derived.
|
|
2358
|
-
const markupSlotIds = new Set(ctx.dynamicElements.map(e => e.slotId))
|
|
2177
|
+
const markupSlotIds = markupSlotIdsOf(ctx)
|
|
2359
2178
|
return generateCsrTemplateWithOpts(node, { inlinableConstants, restSpreadNames, propsObjectName, csrEnv, unsafeLocalNames: effectiveUnsafeLocalNames, deferredChildSlots, loopDepth: -1, markupSlotIds, restPropsName: ctx.restPropsName })
|
|
2360
2179
|
}
|
|
2361
2180
|
|
|
@@ -2736,18 +2555,13 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
2736
2555
|
const transformed = transformExpr(node.expr, node.templateExpr)
|
|
2737
2556
|
// Init-body-only refs would render the literal text "undefined"
|
|
2738
2557
|
// before init's createEffect overwrites the slot (#1128). Emit
|
|
2739
|
-
// an empty placeholder instead.
|
|
2740
|
-
const
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
const isMarkup = opts.markupSlotIds?.has(node.slotId) ?? false
|
|
2747
|
-
return `<!--bf:${node.slotId}-->\${${node.joinArrayChild ? value : escapeTextSlotExpr(expr, isMarkup)}}<!--/-->`
|
|
2748
|
-
}
|
|
2749
|
-
// Bare-splice fallthrough (no `slotId`).
|
|
2750
|
-
return `\${${bareSpliceExpr(node, value)}}`
|
|
2558
|
+
// an empty placeholder instead — a literal, so it bypasses the door.
|
|
2559
|
+
const hole = interp(
|
|
2560
|
+
transformed === UNSAFE_TEMPLATE_EXPR
|
|
2561
|
+
? EMPTY_MARKUP
|
|
2562
|
+
: spliceChildValue(node, transformed, { markupSlotIds: opts.markupSlotIds }),
|
|
2563
|
+
)
|
|
2564
|
+
return node.slotId ? `<!--bf:${node.slotId}-->${hole}<!--/-->` : hole
|
|
2751
2565
|
}
|
|
2752
2566
|
|
|
2753
2567
|
case 'conditional': {
|
|
@@ -2764,7 +2578,7 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
2764
2578
|
const falseBranch = recurse(node.whenFalse)
|
|
2765
2579
|
const trueHtml = node.slotId ? addCondAttrToTemplate(trueBranch, node.slotId) : trueBranch
|
|
2766
2580
|
const falseHtml = node.slotId ? addCondAttrToTemplate(falseBranch, node.slotId) : falseBranch
|
|
2767
|
-
return
|
|
2581
|
+
return interp(conditionalMarkup(transformExpr(node.condition, node.templateCondition), trueHtml, falseHtml))
|
|
2768
2582
|
}
|
|
2769
2583
|
|
|
2770
2584
|
case 'fragment':
|
|
@@ -2844,7 +2658,7 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
2844
2658
|
// See `renderChildScopeArgs`'s docstring — a loop item root still gets
|
|
2845
2659
|
// its slotId, plus the `loopItemRoot` flag (#2833), rather than no
|
|
2846
2660
|
// slot argument at all.
|
|
2847
|
-
return
|
|
2661
|
+
return interp(renderChildCall(nameForRegistryRef(node.name), propsExpr, renderChildScopeArgs(node, keyArg)))
|
|
2848
2662
|
}
|
|
2849
2663
|
|
|
2850
2664
|
case 'loop': {
|
|
@@ -2916,14 +2730,14 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
2916
2730
|
// Leaf `key` stripped — see the irToHtmlTemplate site above.
|
|
2917
2731
|
renderLeaf: (ir) => recurseInLoopBody(stripLeafKeyAttr(ir)),
|
|
2918
2732
|
})
|
|
2919
|
-
mapExpr =
|
|
2733
|
+
mapExpr = interp(mappedRowsMarkup(iterArrayExpr, 'flatMap', node.flatMapCallback.params, body))
|
|
2920
2734
|
} else if (node.preamble) {
|
|
2921
2735
|
// Stage 3 / D4 — template-variant js text with the props rewrite
|
|
2922
2736
|
// applied per segment; JSX leaves render via the loop-body recursion.
|
|
2923
2737
|
const preamble = renderPreamble(node.preamble, { textVariant: 'template', transformJs: (t) => rewritePropsObjectRef(t, propsObjectName ?? null, restPropsName ?? null, { enclosingScope: childScope }), renderLeaf: (ir) => recurseInLoopBody(ir) })
|
|
2924
|
-
mapExpr =
|
|
2738
|
+
mapExpr = interp(mappedRowsMarkup(iterArrayExpr, iterMethod, callbackParam, `{ ${preamble} return \`${childTemplate}\` }`))
|
|
2925
2739
|
} else {
|
|
2926
|
-
mapExpr =
|
|
2740
|
+
mapExpr = interp(mappedRowsMarkup(iterArrayExpr, iterMethod, callbackParam, `\`${childTemplate}\``))
|
|
2927
2741
|
}
|
|
2928
2742
|
return `<!--${loopStartMarker(node.markerId)}-->${mapExpr}<!--${loopEndMarker(node.markerId)}-->`
|
|
2929
2743
|
}
|
|
@@ -2931,7 +2745,7 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
2931
2745
|
case 'if-statement': {
|
|
2932
2746
|
const consequent = recurse(node.consequent)
|
|
2933
2747
|
const alternate = node.alternate ? recurse(node.alternate) : ''
|
|
2934
|
-
return
|
|
2748
|
+
return interp(conditionalMarkup(transformExpr(node.condition, node.templateCondition), consequent, alternate))
|
|
2935
2749
|
}
|
|
2936
2750
|
|
|
2937
2751
|
case 'provider':
|
|
@@ -30,6 +30,9 @@ export const RUNTIME_IMPORT_CANDIDATES = [
|
|
|
30
30
|
'createPortal',
|
|
31
31
|
'provideContext', 'createContext', 'useContext',
|
|
32
32
|
'forwardProps', 'applyRestAttrs', 'splitProps', 'spreadAttrs', 'styleToCss', 'escapeAttr', 'escapeText', 'escapeTextOrNode',
|
|
33
|
+
// Whole-item loop conditional's `bf-loop-i:<key>` anchor comment (#1665,
|
|
34
|
+
// #2795 follow-up) — neutralizes `-` so a key can't close the comment early.
|
|
35
|
+
'escapeCommentText',
|
|
33
36
|
// JSX-element-as-non-children-prop markup brand (#2651) — `bfMarkup` wraps
|
|
34
37
|
// the compiler-built HTML at the producer (renderChild / initChild props);
|
|
35
38
|
// `escapeTextOrMarkup` unwraps it at the claim-plan-'markup' template slot.
|
|
@@ -15,6 +15,7 @@ import { generateInitFunction } from './generate-init.ts'
|
|
|
15
15
|
import { buildReferencesGraph, graphUsedIdentifiers } from './build-references.ts'
|
|
16
16
|
import { addConstantPropRefsToSet } from './init-declarations.ts'
|
|
17
17
|
import { canGenerateStaticTemplate, irToComponentTemplate, generateCsrTemplate } from './html-template.ts'
|
|
18
|
+
import { markupSlotIdsOf } from './markup-slots.ts'
|
|
18
19
|
import { PROPS_PARAM } from './utils.ts'
|
|
19
20
|
import { buildInlinableConstants, csrInlinableConstantsFromCtx } from './emit-registration.ts'
|
|
20
21
|
import { buildEnvFromCtx } from './compute-inlinability.ts'
|
|
@@ -282,10 +283,9 @@ function generateTemplateOnlyMount(ir: ComponentIR, ctx: ClientJsContext): strin
|
|
|
282
283
|
|
|
283
284
|
if (canGenerateStaticTemplate(ir.root, propNamesForStaticCheck, inlinableConstants, unsafeLocalNames)) {
|
|
284
285
|
// `ctx.dynamicElements` is always empty on this template-only path
|
|
285
|
-
// (`needsClientJs(ctx)` gates it) —
|
|
286
|
-
//
|
|
287
|
-
|
|
288
|
-
const markupSlotIds = new Set(ctx.dynamicElements.map(e => e.slotId))
|
|
286
|
+
// (`needsClientJs(ctx)` gates it) — derived through the shared function
|
|
287
|
+
// anyway rather than special-casing "no markup slots here" (#2651).
|
|
288
|
+
const markupSlotIds = markupSlotIdsOf(ctx)
|
|
289
289
|
templateHtml = irToComponentTemplate(ir.root, inlinableConstants, restSpreadNames, ctx.propsObjectName, markupSlotIds, ctx.restPropsName)
|
|
290
290
|
}
|
|
291
291
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which content slots may carry raw markup at INITIAL render — decided once
|
|
3
|
+
* (#2795, consolidating three copies of the same one-liner that were
|
|
4
|
+
* "trusted by comment" to agree with `emit-reactive.ts`'s writer kind).
|
|
5
|
+
*
|
|
6
|
+
* Every non-conditional `ctx.dynamicElements` entry gets a claim-plan
|
|
7
|
+
* writer of `kind: 'markup'` on the REACTIVE side (`emit-reactive.ts`),
|
|
8
|
+
* because its expression may resolve to a live Node or to a `bfMarkup()`-
|
|
9
|
+
* branded JSX-prop value (#1663, #2651). The initial-render template must
|
|
10
|
+
* make the same call for the same slots — `escapeTextOrMarkup` instead of
|
|
11
|
+
* `escapeText` (`safe-html.ts`'s `spliceChildValue`, arm 3) — so first paint
|
|
12
|
+
* and first update agree on whether the slot unwraps a branded value.
|
|
13
|
+
* `client-template-escape-soundness.test.ts` pins that the two sets are
|
|
14
|
+
* equal on emitted output.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { ClientJsContext } from './types.ts'
|
|
18
|
+
|
|
19
|
+
/** The claim-writer kind every `ctx.dynamicElements` slot is emitted with. */
|
|
20
|
+
export const DYNAMIC_ELEMENT_WRITER_KIND = 'markup' as const
|
|
21
|
+
|
|
22
|
+
/** Slot ids whose initial-render splice must be `escapeTextOrMarkup`. */
|
|
23
|
+
export function markupSlotIdsOf(ctx: Pick<ClientJsContext, 'dynamicElements'>): ReadonlySet<string> {
|
|
24
|
+
return new Set(ctx.dynamicElements.map(e => e.slotId))
|
|
25
|
+
}
|