@barefootjs/jsx 0.26.2 → 0.26.4
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/adapters/interface.d.ts +29 -0
- package/dist/adapters/interface.d.ts.map +1 -1
- package/dist/adapters/jsx-adapter.d.ts +9 -0
- package/dist/adapters/jsx-adapter.d.ts.map +1 -1
- package/dist/adapters/loop-bound-names.d.ts.map +1 -1
- package/dist/adapters/parsed-expr-emitter.d.ts +0 -10
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/adapters/test-adapter.d.ts.map +1 -1
- package/dist/analyzer-context.d.ts +11 -1
- package/dist/analyzer-context.d.ts.map +1 -1
- package/dist/analyzer.d.ts +40 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.js +1086 -269
- package/dist/ir-to-client-js/build-references.d.ts.map +1 -1
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts +14 -1
- package/dist/ir-to-client-js/control-flow/plan/branch-loop.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-event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.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/event-delegation.d.ts +16 -1
- package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +39 -0
- 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 +12 -1
- package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/event-delegation.d.ts +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts +19 -0
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +67 -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/dist/ir-to-client-js/plan/build-static-array-child-init.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +29 -4
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/loop-destructure.d.ts.map +1 -1
- package/dist/strip-types.d.ts +18 -0
- package/dist/strip-types.d.ts.map +1 -1
- package/dist/types.d.ts +143 -32
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +277 -33
- package/src/__tests__/client-js-generation.test.ts +18 -5
- package/src/__tests__/compiler-runtime-contract.test.ts +4 -4
- package/src/__tests__/compiler-stress-1244.test.ts +12 -1
- package/src/__tests__/delegated-handler-preamble.test.ts +153 -0
- package/src/__tests__/event-delegation-scope-2367.test.ts +108 -0
- package/src/__tests__/flatmap-segments.test.ts +262 -0
- package/src/__tests__/map-arbitrary-body.test.ts +226 -0
- package/src/__tests__/map-body-no-silent-divergence.test.ts +405 -0
- package/src/__tests__/map-multi-return-body.test.ts +172 -0
- package/src/__tests__/preamble-region-patch.test.ts +150 -0
- package/src/__tests__/static-loop-csr-materialize.test.ts +3 -2
- package/src/__tests__/unsupported-expression.test.ts +20 -2
- package/src/adapters/interface.ts +40 -0
- package/src/adapters/jsx-adapter.ts +10 -0
- package/src/adapters/loop-bound-names.ts +6 -2
- package/src/adapters/parsed-expr-emitter.ts +5 -10
- package/src/adapters/test-adapter.ts +10 -0
- package/src/analyzer-context.ts +35 -1
- package/src/analyzer.ts +162 -24
- package/src/compiler.ts +2 -2
- package/src/expression-parser.ts +27 -19
- package/src/ir-to-client-js/build-references.ts +19 -2
- package/src/ir-to-client-js/collect-elements.ts +69 -13
- package/src/ir-to-client-js/control-flow/plan/branch-loop.ts +14 -1
- package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +18 -5
- package/src/ir-to-client-js/control-flow/plan/build-component-loop.ts +4 -0
- package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +15 -2
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +32 -3
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +26 -3
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +53 -2
- package/src/ir-to-client-js/control-flow/plan/event-delegation.ts +16 -1
- package/src/ir-to-client-js/control-flow/plan/loop.ts +40 -0
- package/src/ir-to-client-js/control-flow/shared.ts +28 -2
- package/src/ir-to-client-js/control-flow/stringify/branch-loop.ts +25 -3
- package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +76 -21
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +65 -1
- package/src/ir-to-client-js/control-flow.ts +11 -0
- package/src/ir-to-client-js/html-template.ts +244 -39
- package/src/ir-to-client-js/imports.ts +1 -1
- package/src/ir-to-client-js/plan/build-static-array-child-init.ts +22 -10
- package/src/ir-to-client-js/reactivity.ts +6 -0
- package/src/ir-to-client-js/types.ts +28 -3
- package/src/jsx-to-ir.ts +1059 -167
- package/src/loop-destructure.ts +9 -5
- package/src/rich-type-refusal.ts +6 -2
- package/src/strip-types.ts +47 -0
- package/src/types.ts +159 -35
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* IR → HTML template string generation and validation.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { AttrValue, IRAttribute, IRNode, IRProp } from '../types.ts'
|
|
5
|
+
import type { AttrValue, FlatMapCallback, IRAttribute, IRNode, IRProp, MapCallbackPreamble } from '../types.ts'
|
|
6
6
|
import { isBooleanAttr } from '../html-constants.ts'
|
|
7
7
|
import { toHtmlAttrName, attrValueToString, quotePropName, PROPS_PARAM, DATA_BF_PH, keyAttrName, loopStartMarker, loopEndMarker, loopItemMarker, freeIdsFromRefs, setIntersects, wrapExprWithLoopParams } from './utils.ts'
|
|
8
8
|
import type { LoopParamSpec } from './utils.ts'
|
|
@@ -276,9 +276,14 @@ function templateAttrExpr(attrName: string, valExpr: string, presenceOrUndefined
|
|
|
276
276
|
// `data-key` / `data-key-N` is a reconciliation contract — every loop item
|
|
277
277
|
// must carry one. Emit unconditionally; if the user passes `key={undefined}`
|
|
278
278
|
// we want it to surface as `data-key="undefined"` (and ultimately a runtime
|
|
279
|
-
// assertion in mapArray) rather than silently fall back to "no key"
|
|
279
|
+
// assertion in mapArray) rather than silently fall back to "no key" —
|
|
280
|
+
// `escapeAttr(undefined)` stringifies to exactly that. The value is escaped
|
|
281
|
+
// like every other dynamic attribute: SSR adapters escape it (their
|
|
282
|
+
// template engines do), so an unescaped `"` in a key — surfaced by the
|
|
283
|
+
// flatmap-expression-body fixture's adversarial keys — corrupted the
|
|
284
|
+
// client-assembled HTML and diverged from the SSR bytes.
|
|
280
285
|
if (attrName === 'data-key' || attrName.startsWith('data-key-')) {
|
|
281
|
-
return `${attrName}="\${${valExpr}}"`
|
|
286
|
+
return `${attrName}="\${${escapeAttrValueExpr(valExpr)}}"`
|
|
282
287
|
}
|
|
283
288
|
return `\${(${valExpr}) != null ? '${attrName}="' + ${escapeAttrValueExpr(valExpr)} + '"' : ''}`
|
|
284
289
|
}
|
|
@@ -556,6 +561,169 @@ function itemAnchorTemplate(keyExpr: string): string {
|
|
|
556
561
|
return `<!--${loopItemMarker('${' + keyExpr + '}')}-->`
|
|
557
562
|
}
|
|
558
563
|
|
|
564
|
+
/**
|
|
565
|
+
* The ONLY door from a structured `.map()` preamble to emitted text (Stage 3
|
|
566
|
+
* root cure, spec/callback-fidelity.md). `js` segments contribute their JS
|
|
567
|
+
* text (template variant when requested, then the caller's context transform —
|
|
568
|
+
* e.g. the loop-param accessor wrap); `jsx` segments render their compiled IR
|
|
569
|
+
* as a template-literal HTML string. There is no sentinel substitution because
|
|
570
|
+
* there is no sentinel: mixed content stays structured until this function.
|
|
571
|
+
* Emitters that cannot host a string-templated preamble must not call this —
|
|
572
|
+
* they refuse at plan dispatch instead of splicing.
|
|
573
|
+
*/
|
|
574
|
+
export function renderPreamble(
|
|
575
|
+
preamble: Pick<MapCallbackPreamble, 'segments'>,
|
|
576
|
+
opts: {
|
|
577
|
+
/** Pick `templateText` on js segments (hydrate/CSR template contexts). */
|
|
578
|
+
textVariant?: 'client' | 'template'
|
|
579
|
+
/** Context transform for js text (e.g. wrapLoopParamAsAccessor). */
|
|
580
|
+
transformJs?: (text: string) => string
|
|
581
|
+
/** Context-appropriate leaf renderer (an irToHtmlTemplate variant). */
|
|
582
|
+
renderLeaf: (ir: IRNode) => string
|
|
583
|
+
/**
|
|
584
|
+
* When true, `renderLeaf` output is spliced verbatim — the renderer
|
|
585
|
+
* supplies its own delimiters (e.g. the flatMap descriptor form
|
|
586
|
+
* `({ k, h })`). Default wraps each leaf in a template literal.
|
|
587
|
+
*/
|
|
588
|
+
rawLeaf?: boolean
|
|
589
|
+
},
|
|
590
|
+
): string {
|
|
591
|
+
let out = ''
|
|
592
|
+
for (const seg of preamble.segments) {
|
|
593
|
+
if (seg.kind === 'js') {
|
|
594
|
+
const text = opts.textVariant === 'template' ? (seg.templateText ?? seg.text) : seg.text
|
|
595
|
+
out += opts.transformJs ? opts.transformJs(text) : text
|
|
596
|
+
} else if (opts.rawLeaf) {
|
|
597
|
+
out += opts.renderLeaf(escapeLeafTextExpressions(seg.ir))
|
|
598
|
+
} else {
|
|
599
|
+
out += '`' + opts.renderLeaf(escapeLeafTextExpressions(seg.ir)) + '`'
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return out
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Project a flatMap segment leaf's `key={...}` attribute into a runtime key
|
|
607
|
+
* expression, or `null` when the leaf declares none. Template-literal keys
|
|
608
|
+
* (`key={\`${it.id}:${tag}\`}`) are first-class here — unlike the loop-level
|
|
609
|
+
* `extractLoopKey`, the expression is evaluated inside the flatMap body where
|
|
610
|
+
* the callback params are in scope, so any expression form works.
|
|
611
|
+
*/
|
|
612
|
+
export function flatMapLeafKeyExpr(ir: IRNode): string | null {
|
|
613
|
+
if (ir.type !== 'element') return null
|
|
614
|
+
const keyAttr = ir.attrs.find((a) => a.name === 'key')
|
|
615
|
+
if (!keyAttr) return null
|
|
616
|
+
switch (keyAttr.value.kind) {
|
|
617
|
+
case 'expression':
|
|
618
|
+
return `(${keyAttr.value.expr})`
|
|
619
|
+
case 'literal':
|
|
620
|
+
return JSON.stringify(keyAttr.value.value)
|
|
621
|
+
case 'template':
|
|
622
|
+
return attrValueToString(keyAttr.value)
|
|
623
|
+
default:
|
|
624
|
+
return null
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/** Copy of `ir` with the loop `key` attribute removed (element leaves only). */
|
|
629
|
+
function stripLeafKeyAttr(ir: IRNode): IRNode {
|
|
630
|
+
if (ir.type !== 'element') return ir
|
|
631
|
+
return { ...ir, attrs: ir.attrs.filter((a) => a.name !== 'key') }
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Render a `FlatMapCallback` as the CLIENT-SIDE descriptor body for
|
|
636
|
+
* `mapArray` (the reconciliation twin of the string-template rendering in
|
|
637
|
+
* `irToHtmlTemplate`'s `'loop'` case). Each JSX leaf becomes
|
|
638
|
+
* `({ k: <keyExpr>, h: \`<html>\` })` — the flatMap flattens descriptors,
|
|
639
|
+
* `mapArray` keys on `d.k` (index fallback), and the emitted renderItem
|
|
640
|
+
* builds/patches the leaf element from `d.h`.
|
|
641
|
+
*
|
|
642
|
+
* Runs in the init scope where the loop SOURCE items are plain values (the
|
|
643
|
+
* flatMap executes inside the `mapArray` accessor, BEFORE per-item signals
|
|
644
|
+
* exist), so neither js segments nor leaf HTML get the accessor wrap — refs
|
|
645
|
+
* stay `t.title`, not `t().title`. `data-key` is deliberately NOT emitted in
|
|
646
|
+
* the leaf HTML: reconciliation identity is stamped by `mapArray` via
|
|
647
|
+
* `setAttribute`, matching the SSR side (which never emits it for flatMap
|
|
648
|
+
* leaves).
|
|
649
|
+
*/
|
|
650
|
+
export function renderFlatMapClientBody(
|
|
651
|
+
cb: Pick<FlatMapCallback, 'segments'>,
|
|
652
|
+
restSpreadNames?: Set<string>,
|
|
653
|
+
): string {
|
|
654
|
+
return renderPreamble(cb, {
|
|
655
|
+
textVariant: 'client',
|
|
656
|
+
rawLeaf: true,
|
|
657
|
+
renderLeaf: (ir) => {
|
|
658
|
+
const key = flatMapLeafKeyExpr(ir)
|
|
659
|
+
const html = irToHtmlTemplate(stripLeafKeyAttr(ir), restSpreadNames, 1, undefined, undefined, true)
|
|
660
|
+
return `({ k: ${key ?? 'undefined'}, h: \`${html}\` })`
|
|
661
|
+
},
|
|
662
|
+
})
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** True when any segment leaf declares a `key` — drives the mapArray keyFn. */
|
|
666
|
+
export function flatMapCallbackHasKeyedLeaf(cb: Pick<FlatMapCallback, 'segments'>): boolean {
|
|
667
|
+
return cb.segments.some((s) => s.kind === 'jsx' && flatMapLeafKeyExpr(s.ir) !== null)
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Synthesize the client descriptor body for a flatMap PROJECTION loop —
|
|
672
|
+
* one whose only child is a nested `IRLoop` lowered from
|
|
673
|
+
* `flatMap(it => it.tags.map(tag => <li/>))`. The neutral IR is the single
|
|
674
|
+
* carrier: SSR adapters templatize the nested loop natively, and this
|
|
675
|
+
* derives the `mapArray` accessor's flatten projection from the SAME inner
|
|
676
|
+
* loop — `<chained-inner>.map((tag, i) => ({ k: <inner.key>, h: `<leaf>` }))`.
|
|
677
|
+
* Runs in the accessor context (plain source items, no per-item signals),
|
|
678
|
+
* so leaf refs stay unwrapped. Leaf `key` attrs were already stripped at IR
|
|
679
|
+
* build (`stripLoopLeafKeyAttrs`); the inner loop's `key` FIELD supplies
|
|
680
|
+
* `k`.
|
|
681
|
+
*/
|
|
682
|
+
export function renderFlatMapProjectionClientBody(
|
|
683
|
+
inner: Extract<IRNode, { type: 'loop' }>,
|
|
684
|
+
restSpreadNames?: Set<string>,
|
|
685
|
+
): string {
|
|
686
|
+
const chained = applyLoopChain(inner)
|
|
687
|
+
const params = inner.index ? `(${inner.param}, ${inner.index})` : `(${inner.param})`
|
|
688
|
+
const key = inner.key ? `(${inner.key})` : 'undefined'
|
|
689
|
+
const html = inner.children
|
|
690
|
+
.map((c) => irToHtmlTemplate(escapeLeafTextExpressions(c), restSpreadNames, 1, undefined, undefined, true))
|
|
691
|
+
.join('')
|
|
692
|
+
return `${chained}.map(${params} => ({ k: ${key}, h: \`${html}\` }))`
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* SSR/CSR escaping parity for preamble leaves, decided once at the door: a
|
|
697
|
+
* JSX-runtime SSR adapter renders the leaf's raw JSX and auto-escapes text
|
|
698
|
+
* interpolations (`{c}`), so the client's HTML-string lowering must escape the
|
|
699
|
+
* same positions — wrap every text-position expression in `escapeText(...)`.
|
|
700
|
+
* Pure (returns a transformed copy); the neutral IR is never mutated with a
|
|
701
|
+
* client-only concern. Attribute values already flow through the template
|
|
702
|
+
* emitters' own attr escaping.
|
|
703
|
+
*/
|
|
704
|
+
function escapeLeafTextExpressions(ir: IRNode): IRNode {
|
|
705
|
+
switch (ir.type) {
|
|
706
|
+
case 'element':
|
|
707
|
+
return { ...ir, children: ir.children.map(escapeLeafTextExpressions) }
|
|
708
|
+
case 'fragment':
|
|
709
|
+
return { ...ir, children: ir.children.map(escapeLeafTextExpressions) }
|
|
710
|
+
case 'expression': {
|
|
711
|
+
if (ir.expr === 'null' || ir.expr === 'undefined') return ir
|
|
712
|
+
// Already-wrapped or slotted expressions keep their existing handling.
|
|
713
|
+
if (ir.slotId || ir.expr.trimStart().startsWith('escapeText(')) return ir
|
|
714
|
+
return { ...ir, expr: `escapeText((${ir.expr}))`, templateExpr: ir.templateExpr ? `escapeText((${ir.templateExpr}))` : ir.templateExpr }
|
|
715
|
+
}
|
|
716
|
+
case 'conditional':
|
|
717
|
+
return {
|
|
718
|
+
...ir,
|
|
719
|
+
whenTrue: escapeLeafTextExpressions(ir.whenTrue),
|
|
720
|
+
whenFalse: ir.whenFalse ? escapeLeafTextExpressions(ir.whenFalse) : ir.whenFalse,
|
|
721
|
+
}
|
|
722
|
+
default:
|
|
723
|
+
return ir
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
559
727
|
export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, loopDepth = 0, loopParams?: ReadonlyArray<string | LoopParamSpec>, branchSlotsVar?: string, insideLoop = false, inHoistedChildren = false): string {
|
|
560
728
|
const recurse = (n: IRNode): string => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar, insideLoop, inHoistedChildren)
|
|
561
729
|
const wrapExpr = (expr: string) => wrapExprWithLoopParams(expr, loopParams)
|
|
@@ -624,20 +792,29 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
|
|
|
624
792
|
// as HTML (template.innerHTML), so re-escape for the HTML parser.
|
|
625
793
|
return escapeHtml(node.value)
|
|
626
794
|
|
|
627
|
-
case 'expression':
|
|
795
|
+
case 'expression': {
|
|
628
796
|
if (node.expr === 'null' || node.expr === 'undefined') return ''
|
|
797
|
+
const inner = wrapInterpolation(wrapExpr(node.expr))
|
|
798
|
+
// Stage 3 / D4 — an element-array child ({out}) built by an arbitrary
|
|
799
|
+
// .map() preamble is an array of HTML strings; join it rather than let
|
|
800
|
+
// `${[...]}` `String`-comma-collapse it. Only reached on a JS-runtime
|
|
801
|
+
// adapter (the flag is set in Phase 1 only there); a plain local can be
|
|
802
|
+
// read twice safely.
|
|
803
|
+
const valueExpr = node.joinArrayChild
|
|
804
|
+
? `Array.isArray(${inner}) ? ${inner}.join('') : (${inner} ?? '')`
|
|
805
|
+
: inner
|
|
629
806
|
if (node.slotId) {
|
|
630
|
-
const inner = wrapInterpolation(wrapExpr(node.expr))
|
|
631
807
|
// In branch-slot context `wrapInterpolation` routes the value
|
|
632
808
|
// through `__bfSlot`, which returns raw `<!--bf-slot:N-->` markers
|
|
633
809
|
// for live `Node` values (spliced back by `insert()`). Escaping
|
|
634
810
|
// would corrupt those markers and drop slotted content (#1694
|
|
635
811
|
// regression). `__bfSlot` owns coercion of its own value, so the
|
|
636
812
|
// text-escape applies only to the non-slot (plain text) form.
|
|
637
|
-
const slotted = branchSlotsVar ?
|
|
813
|
+
const slotted = branchSlotsVar || node.joinArrayChild ? valueExpr : escapeTextSlotExpr(valueExpr)
|
|
638
814
|
return `<!--bf:${node.slotId}-->\${${slotted}}<!--/-->`
|
|
639
815
|
}
|
|
640
|
-
return `\${${
|
|
816
|
+
return `\${${valueExpr}}`
|
|
817
|
+
}
|
|
641
818
|
|
|
642
819
|
case 'conditional': {
|
|
643
820
|
const trueBranch = recurse(node.whenTrue)
|
|
@@ -740,15 +917,21 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
|
|
|
740
917
|
let mapExpr: string
|
|
741
918
|
|
|
742
919
|
if (node.flatMapCallback) {
|
|
743
|
-
// Complex flatMap:
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
920
|
+
// Complex flatMap: the body is structured segments, rendered through
|
|
921
|
+
// the same single door as map preambles.
|
|
922
|
+
// Leaf `key` is stripped from the string form: SSR (Hono rawBody)
|
|
923
|
+
// never emits data-key for flatMap leaves, and reconciliation
|
|
924
|
+
// identity is stamped by mapArray via setAttribute — emitting it
|
|
925
|
+
// here was the CSR/SSR data-key asymmetry (unescaped, client-only).
|
|
926
|
+
const body = renderPreamble(node.flatMapCallback, {
|
|
927
|
+
renderLeaf: (ir) => irToHtmlTemplate(stripLeafKeyAttr(ir), restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar, insideLoop),
|
|
928
|
+
})
|
|
749
929
|
mapExpr = `\${${wrappedArray}.flatMap(${node.flatMapCallback.params} => ${body}).join('')}`
|
|
750
|
-
} else if (node.
|
|
751
|
-
|
|
930
|
+
} else if (node.preamble) {
|
|
931
|
+
// Stage 3 / D4 — render JSX leaves in an arbitrary array-builder
|
|
932
|
+
// preamble (the hydrate-template context uses the bare loop param).
|
|
933
|
+
const preamble = renderPreamble(node.preamble, { textVariant: 'client', renderLeaf: (ir) => irToHtmlTemplate(ir, restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar, insideLoop) })
|
|
934
|
+
mapExpr = `\${${wrappedArray}.${iterMethod}(${callbackParam} => { ${preamble} return \`${childTemplate}\` }).join('')}`
|
|
752
935
|
} else {
|
|
753
936
|
mapExpr = `\${${wrappedArray}.${iterMethod}(${callbackParam} => \`${childTemplate}\`).join('')}`
|
|
754
937
|
}
|
|
@@ -1138,12 +1321,18 @@ export function irToPlaceholderTemplate(node: IRNode, restSpreadNames?: Set<stri
|
|
|
1138
1321
|
// as HTML (template.innerHTML), so re-escape for the HTML parser.
|
|
1139
1322
|
return escapeHtml(node.value)
|
|
1140
1323
|
|
|
1141
|
-
case 'expression':
|
|
1324
|
+
case 'expression': {
|
|
1142
1325
|
if (node.expr === 'null' || node.expr === 'undefined') return ''
|
|
1326
|
+
const wrapped = wrapExpr(node.expr)
|
|
1327
|
+
// Stage 3 / D4 — join an element-array child ({out}) built by the preamble.
|
|
1328
|
+
const value = node.joinArrayChild
|
|
1329
|
+
? `Array.isArray(${wrapped}) ? ${wrapped}.join('') : (${wrapped} ?? '')`
|
|
1330
|
+
: wrapped
|
|
1143
1331
|
if (node.slotId) {
|
|
1144
|
-
return `<!--bf:${node.slotId}-->\${${escapeTextSlotExpr(
|
|
1332
|
+
return `<!--bf:${node.slotId}-->\${${node.joinArrayChild ? value : escapeTextSlotExpr(wrapped)}}<!--/-->`
|
|
1145
1333
|
}
|
|
1146
|
-
return `\${${
|
|
1334
|
+
return `\${${value}}`
|
|
1335
|
+
}
|
|
1147
1336
|
|
|
1148
1337
|
case 'conditional': {
|
|
1149
1338
|
const trueBranch = recurse(node.whenTrue)
|
|
@@ -1180,14 +1369,15 @@ export function irToPlaceholderTemplate(node: IRNode, restSpreadNames?: Set<stri
|
|
|
1180
1369
|
const iterMethod = node.method ?? 'map'
|
|
1181
1370
|
let mapExpr: string
|
|
1182
1371
|
if (node.flatMapCallback) {
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
}
|
|
1372
|
+
const body = renderPreamble(node.flatMapCallback, {
|
|
1373
|
+
// Leaf `key` stripped — see the irToHtmlTemplate site above.
|
|
1374
|
+
renderLeaf: (ir) => irToPlaceholderTemplate(stripLeafKeyAttr(ir), restSpreadNames, loopDepth + 1, loopParams),
|
|
1375
|
+
})
|
|
1188
1376
|
mapExpr = `\${${wrappedArray}.flatMap(${node.flatMapCallback.params} => ${body}).join('')}`
|
|
1189
|
-
} else if (node.
|
|
1190
|
-
|
|
1377
|
+
} else if (node.preamble) {
|
|
1378
|
+
// Stage 3 / D4 — render JSX leaves in an arbitrary array-builder preamble.
|
|
1379
|
+
const preamble = renderPreamble(node.preamble, { textVariant: 'client', renderLeaf: (ir) => irToPlaceholderTemplate(ir, restSpreadNames, loopDepth + 1, loopParams) })
|
|
1380
|
+
mapExpr = `\${${wrappedArray}.${iterMethod}(${callbackParam} => { ${preamble} return \`${childTemplate}\` }).join('')}`
|
|
1191
1381
|
} else {
|
|
1192
1382
|
mapExpr = `\${${wrappedArray}.${iterMethod}(${callbackParam} => \`${childTemplate}\`).join('')}`
|
|
1193
1383
|
}
|
|
@@ -1553,12 +1743,18 @@ function irToComponentTemplateWithOpts(node: IRNode, opts: TemplateOptions): str
|
|
|
1553
1743
|
// as HTML (template.innerHTML), so re-escape for the HTML parser.
|
|
1554
1744
|
return escapeHtml(node.value)
|
|
1555
1745
|
|
|
1556
|
-
case 'expression':
|
|
1746
|
+
case 'expression': {
|
|
1557
1747
|
if (node.expr === 'null' || node.expr === 'undefined') return ''
|
|
1748
|
+
const wrapped = transformExpr(node.expr, node.templateExpr)
|
|
1749
|
+
// Stage 3 / D4 — join an element-array child ({out}) built by the preamble.
|
|
1750
|
+
const value = node.joinArrayChild
|
|
1751
|
+
? `Array.isArray(${wrapped}) ? ${wrapped}.join('') : (${wrapped} ?? '')`
|
|
1752
|
+
: wrapped
|
|
1558
1753
|
if (node.slotId) {
|
|
1559
|
-
return `<!--bf:${node.slotId}-->\${${
|
|
1754
|
+
return `<!--bf:${node.slotId}-->\${${node.joinArrayChild ? value : escapeTextSlotExpr(wrapped)}}<!--/-->`
|
|
1560
1755
|
}
|
|
1561
|
-
return `\${${
|
|
1756
|
+
return `\${${value}}`
|
|
1757
|
+
}
|
|
1562
1758
|
|
|
1563
1759
|
case 'conditional': {
|
|
1564
1760
|
// A client-only conditional (auto-deferred brand read or manual
|
|
@@ -2134,10 +2330,14 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
2134
2330
|
// before init's createEffect overwrites the slot (#1128). Emit
|
|
2135
2331
|
// an empty placeholder instead.
|
|
2136
2332
|
const expr = transformed === UNSAFE_TEMPLATE_EXPR ? "''" : transformed
|
|
2333
|
+
// Stage 3 / D4 — join an element-array child ({out}) built by the preamble.
|
|
2334
|
+
const value = node.joinArrayChild
|
|
2335
|
+
? `Array.isArray(${expr}) ? ${expr}.join('') : (${expr} ?? '')`
|
|
2336
|
+
: expr
|
|
2137
2337
|
if (node.slotId) {
|
|
2138
|
-
return `<!--bf:${node.slotId}-->\${${escapeTextSlotExpr(expr)}}<!--/-->`
|
|
2338
|
+
return `<!--bf:${node.slotId}-->\${${node.joinArrayChild ? value : escapeTextSlotExpr(expr)}}<!--/-->`
|
|
2139
2339
|
}
|
|
2140
|
-
return `\${${
|
|
2340
|
+
return `\${${value}}`
|
|
2141
2341
|
}
|
|
2142
2342
|
|
|
2143
2343
|
case 'conditional': {
|
|
@@ -2287,16 +2487,21 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
2287
2487
|
const iterMethod = node.method ?? 'map'
|
|
2288
2488
|
let mapExpr: string
|
|
2289
2489
|
if (node.flatMapCallback) {
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2490
|
+
// Module-scope template context: pick the templateText variant
|
|
2491
|
+
// (destructured-prop refs rewritten to _p.xxx) and apply the
|
|
2492
|
+
// props-object rewrite to js segments only — a rendered leaf handles
|
|
2493
|
+
// its own props context via recurseInLoopBody.
|
|
2494
|
+
const body = renderPreamble(node.flatMapCallback, {
|
|
2495
|
+
textVariant: 'template',
|
|
2496
|
+
transformJs: (t) => applyPropsRewrite(t, propsObjectName ?? null),
|
|
2497
|
+
// Leaf `key` stripped — see the irToHtmlTemplate site above.
|
|
2498
|
+
renderLeaf: (ir) => recurseInLoopBody(stripLeafKeyAttr(ir)),
|
|
2499
|
+
})
|
|
2296
2500
|
mapExpr = `\${${iterArrayExpr}.flatMap(${node.flatMapCallback.params} => ${body}).join('')}`
|
|
2297
|
-
} else if (node.
|
|
2298
|
-
|
|
2299
|
-
|
|
2501
|
+
} else if (node.preamble) {
|
|
2502
|
+
// Stage 3 / D4 — template-variant js text with the props rewrite
|
|
2503
|
+
// applied per segment; JSX leaves render via the loop-body recursion.
|
|
2504
|
+
const preamble = renderPreamble(node.preamble, { textVariant: 'template', transformJs: (t) => applyPropsRewrite(t, propsObjectName ?? null), renderLeaf: (ir) => recurseInLoopBody(ir) })
|
|
2300
2505
|
mapExpr = `\${${iterArrayExpr}.${iterMethod}(${callbackParam} => { ${preamble} return \`${childTemplate}\` }).join('')}`
|
|
2301
2506
|
} else {
|
|
2302
2507
|
mapExpr = `\${${iterArrayExpr}.${iterMethod}(${callbackParam} => \`${childTemplate}\`).join('')}`
|
|
@@ -8,7 +8,7 @@ import { isClientBuiltinName } from '../builtins.ts'
|
|
|
8
8
|
// All exports from @barefootjs/client/runtime that may be used in generated code
|
|
9
9
|
export const RUNTIME_IMPORT_CANDIDATES = [
|
|
10
10
|
'createSignal', 'createMemo', 'createEffect', 'onCleanup', 'onMount',
|
|
11
|
-
'hydrate', 'insert', 'reconcileElements', 'getLoopChildren', 'getLoopNodes', 'mapArray', 'mapArrayAnchored', 'createDisposableEffect',
|
|
11
|
+
'hydrate', 'insert', 'reconcileElements', 'getLoopChildren', 'getLoopNodes', 'mapArray', 'mapArrayAnchored', 'patchLeaf', 'patchSlotRange', 'createDisposableEffect',
|
|
12
12
|
'createComponent', 'renderChild', 'registerComponent', 'registerTemplate', 'initChild', 'upsertChild', 'updateClientMarker',
|
|
13
13
|
'createPortal',
|
|
14
14
|
'provideContext', 'createContext', 'useContext',
|
|
@@ -17,11 +17,23 @@
|
|
|
17
17
|
* stringifier never inspects raw IR.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import type { IRLoopChildComponent } from '../../types.ts'
|
|
21
|
-
import type { NestedLoop, TopLevelLoop } from '../types.ts'
|
|
22
|
-
import type { ClientJsContext } from '../types.ts'
|
|
20
|
+
import type { IRLoopChildComponent, MapCallbackPreamble } from '../../types.ts'
|
|
21
|
+
import type { ClientJsContext, NestedLoop, TopLevelLoop } from '../types.ts'
|
|
23
22
|
import { quotePropName, varSlotId, attrValueToString, buildLoopChildIndexExpr } from '../utils.ts'
|
|
24
|
-
import { irChildrenToJsExpr } from '../html-template.ts'
|
|
23
|
+
import { irChildrenToJsExpr, renderPreamble, irToHtmlTemplate } from '../html-template.ts'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Render a loop preamble for the static-array init context. Static `forEach`
|
|
27
|
+
* binds params as plain values (see `buildStaticPropsExpr` note), so neither
|
|
28
|
+
* js text nor leaf-JSX param reads get accessor-wrapped.
|
|
29
|
+
*/
|
|
30
|
+
function staticPreludeStatements(preamble: MapCallbackPreamble | undefined): string[] {
|
|
31
|
+
return preamble
|
|
32
|
+
? [renderPreamble(preamble, {
|
|
33
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined, true),
|
|
34
|
+
})]
|
|
35
|
+
: []
|
|
36
|
+
}
|
|
25
37
|
import { buildCompSelector } from '../control-flow/shared.ts'
|
|
26
38
|
|
|
27
39
|
/** The inline prop shape carried on `IRLoopChildComponent.props`. */
|
|
@@ -94,7 +106,7 @@ function buildSingleCompPlan(
|
|
|
94
106
|
arrayExpr: elem.array,
|
|
95
107
|
param: elem.param,
|
|
96
108
|
indexParam: elem.index || '__idx',
|
|
97
|
-
outerPreludeStatements: elem.
|
|
109
|
+
outerPreludeStatements: staticPreludeStatements(elem.preamble),
|
|
98
110
|
propsExpr: buildStaticPropsExpr(props),
|
|
99
111
|
}
|
|
100
112
|
}
|
|
@@ -113,7 +125,7 @@ function buildOuterNestedPlan(
|
|
|
113
125
|
param: elem.param,
|
|
114
126
|
indexParam,
|
|
115
127
|
offsetExpr: buildLoopChildIndexExpr(indexParam, elem.offset),
|
|
116
|
-
outerPreludeStatements: elem.
|
|
128
|
+
outerPreludeStatements: staticPreludeStatements(elem.preamble),
|
|
117
129
|
propsExpr: buildStaticPropsExpr(comp.props),
|
|
118
130
|
}
|
|
119
131
|
}
|
|
@@ -142,13 +154,13 @@ function buildInnerLoopNestedPlan(
|
|
|
142
154
|
outerParam: elem.param,
|
|
143
155
|
outerIndexParam,
|
|
144
156
|
outerOffsetExpr: buildLoopChildIndexExpr(outerIndexParam, elem.offset),
|
|
145
|
-
outerPreludeStatements: elem.
|
|
157
|
+
outerPreludeStatements: staticPreludeStatements(elem.preamble),
|
|
146
158
|
innerContainerSlotId: innerLoop.containerSlotId ?? null,
|
|
147
159
|
innerArrayExpr: innerLoop.array,
|
|
148
160
|
innerParam: innerLoop.param,
|
|
149
161
|
innerIndexParam,
|
|
150
162
|
innerOffsetExpr: buildLoopChildIndexExpr(innerIndexParam, innerLoop.offset),
|
|
151
|
-
innerPreludeStatements: innerLoop.
|
|
163
|
+
innerPreludeStatements: staticPreludeStatements(innerLoop.preamble),
|
|
152
164
|
depth: innerLoop.depth,
|
|
153
165
|
comps,
|
|
154
166
|
}
|
|
@@ -189,11 +201,11 @@ function buildComponentRootedInnerLoopPlan(
|
|
|
189
201
|
// position, so there's no synthetic fallback and index-less loops keep
|
|
190
202
|
// byte-identical output.
|
|
191
203
|
outerIndexParam: elem.index,
|
|
192
|
-
outerPreludeStatements: elem.
|
|
204
|
+
outerPreludeStatements: staticPreludeStatements(elem.preamble),
|
|
193
205
|
innerArrayExpr: innerLoop.array,
|
|
194
206
|
innerParam: innerLoop.param,
|
|
195
207
|
innerIndexParam: innerLoop.index,
|
|
196
|
-
innerPreludeStatements: innerLoop.
|
|
208
|
+
innerPreludeStatements: staticPreludeStatements(innerLoop.preamble),
|
|
197
209
|
depth: innerLoop.depth,
|
|
198
210
|
comps,
|
|
199
211
|
}
|
|
@@ -570,6 +570,12 @@ export function collectLoopChildReactiveTexts(
|
|
|
570
570
|
...stopAt<boolean>('loop', 'async', 'ifStatement'),
|
|
571
571
|
expression: ({ node: n, scope: insideConditional }) => {
|
|
572
572
|
if (!n.slotId) return
|
|
573
|
+
// #2389 — a preamble-patched region (see `IRLoop.preambleRegions`) has
|
|
574
|
+
// its own `patchSlotRange`-based effect (`preambleRegions` in the
|
|
575
|
+
// client-JS loop plan); it must never ALSO become a `reactiveTexts`
|
|
576
|
+
// entry, which would patch it via `.textContent` and corrupt markup
|
|
577
|
+
// (a `joinArrayChild` region's value is raw HTML, not text).
|
|
578
|
+
if (n.preambleRegion) return
|
|
573
579
|
const originFreeIds = freeIdsFromRefs(n.origin?.freeRefs)
|
|
574
580
|
const expanded = expandConstantForReactivity(n.expr, ctx, originFreeIds)
|
|
575
581
|
// Include if expression reads signals OR references the loop parameter
|
|
@@ -22,6 +22,8 @@ import type {
|
|
|
22
22
|
ImportInfo,
|
|
23
23
|
TypeInfo,
|
|
24
24
|
TypeDefinition,
|
|
25
|
+
MapCallbackPreamble,
|
|
26
|
+
PreambleRegionSource,
|
|
25
27
|
} from '../types.ts'
|
|
26
28
|
import type { CsrInlinabilityMap } from './csr-substitute.ts'
|
|
27
29
|
import type { SkeletonSlotPaths } from './html-template.ts'
|
|
@@ -309,6 +311,29 @@ export interface LoopCore {
|
|
|
309
311
|
* `IRLoop.objectIteration`.
|
|
310
312
|
*/
|
|
311
313
|
objectIteration?: 'entries' | 'keys' | 'values'
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Client-side descriptor body for a flatMap loop carried as structured
|
|
317
|
+
* segments (`IRLoop.flatMapCallback`). `body` is the callback body
|
|
318
|
+
* rendered so each JSX leaf becomes a `({ k, h })` descriptor
|
|
319
|
+
* (`renderFlatMapClientBody`); the plan builder wires
|
|
320
|
+
* `mapArray(() => <chained>.flatMap(<params> => <body>), …)` so the
|
|
321
|
+
* runtime reconciles the FLATTENED leaves — never the un-flattened
|
|
322
|
+
* source items (which loses leaves at hydration and crashes on adds
|
|
323
|
+
* against an empty item template). `keyed` is true when any leaf
|
|
324
|
+
* declares a `key`, driving the `d.k`-based keyFn.
|
|
325
|
+
*/
|
|
326
|
+
flatMapClient?: { params: string; body: string; keyed: boolean }
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Loop-body expression children classified as preamble-patched regions
|
|
330
|
+
* (#2389), threaded from `IRLoop.preambleRegions`. Consumed only by the
|
|
331
|
+
* top-level and branch **plain** loop-plan builders (`build-loop.ts`,
|
|
332
|
+
* `build-branch-loop.ts`) — the composite / component / static / anchored
|
|
333
|
+
* shapes leave this unconsumed (stale — same pre-existing freeze, not a
|
|
334
|
+
* new regression) pending a follow-up.
|
|
335
|
+
*/
|
|
336
|
+
preambleRegions?: readonly PreambleRegionSource[]
|
|
312
337
|
}
|
|
313
338
|
|
|
314
339
|
/**
|
|
@@ -356,7 +381,7 @@ export interface BranchLoop extends LoopCore {
|
|
|
356
381
|
index: string | null // Index parameter (e.g., 'i')
|
|
357
382
|
template: string // HTML template for each item
|
|
358
383
|
containerSlotId: string // bf slot ID of the container element (e.g., 's1' for <ul bf="s1">)
|
|
359
|
-
|
|
384
|
+
preamble?: MapCallbackPreamble
|
|
360
385
|
// Composite loop fields (loops whose body contains child components)
|
|
361
386
|
nestedComponents?: IRLoopChildComponent[]
|
|
362
387
|
innerLoops?: NestedLoop[]
|
|
@@ -442,7 +467,7 @@ export interface NestedLoop extends LoopCore {
|
|
|
442
467
|
* `renderItem` callback so locals referenced by the cloned-template IIFE
|
|
443
468
|
* (and any subsequent reads) are in scope (#1052).
|
|
444
469
|
*/
|
|
445
|
-
|
|
470
|
+
preamble?: MapCallbackPreamble
|
|
446
471
|
/** Whether the inner array references the outer loop param (needs reactive mapArray) */
|
|
447
472
|
refsOuterParam?: boolean
|
|
448
473
|
/** Child components inside inner loop items (for initChild/createComponent) */
|
|
@@ -599,7 +624,7 @@ export interface TopLevelLoop extends LoopCore {
|
|
|
599
624
|
raw: string // Full comparator body for client JS
|
|
600
625
|
}
|
|
601
626
|
chainOrder?: 'filter-sort' | 'sort-filter'
|
|
602
|
-
|
|
627
|
+
preamble?: MapCallbackPreamble
|
|
603
628
|
}
|
|
604
629
|
|
|
605
630
|
/**
|