@barefootjs/jsx 0.34.0 → 0.35.1
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-context.d.ts.map +1 -1
- 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 +1072 -903
- package/dist/ir-to-client-js/collect-elements.d.ts +22 -2
- 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-inner-loop.d.ts +14 -2
- 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-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-child-arm.d.ts +6 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.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 +5 -2
- 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/inner-loop.d.ts +24 -44
- package/dist/ir-to-client-js/control-flow/plan/inner-loop.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-child-arm.d.ts +14 -0
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.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/inner-loop.d.ts +6 -14
- package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts +1 -3
- package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts +19 -3
- 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/reactive-effects.d.ts +5 -1
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.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 +54 -0
- 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/reactivity.d.ts +62 -18
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- 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/types.d.ts +18 -2
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- 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/reactivity-checker.d.ts +18 -0
- package/dist/reactivity-checker.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 +138 -22
- package/src/__tests__/binding-scope-ratchet.test.ts +25 -2
- package/src/__tests__/build-plain-row-consolidation.test.ts +120 -0
- package/src/__tests__/child-prop-fallback-wrap.test.ts +4 -2
- package/src/__tests__/client-template-escape-soundness.test.ts +538 -0
- package/src/__tests__/compiler-stress-1244.test.ts +8 -8
- package/src/__tests__/csr-template-loop-shadowing.test.ts +4 -2
- package/src/__tests__/flatmap-segments.test.ts +1 -1
- package/src/__tests__/issue-2868-cond-arm-tag-collision.test.ts +185 -0
- package/src/__tests__/issue-2869-attr-dedup-guard.test.ts +141 -0
- 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-index-reactivity.test.ts +208 -0
- package/src/__tests__/loop-item-conditional-codegen.test.ts +4 -2
- package/src/__tests__/map-arbitrary-body.test.ts +1 -1
- package/src/__tests__/map-body-brand-checker.test.ts +176 -0
- package/src/__tests__/nested-loop-index-param.test.ts +41 -8
- package/src/__tests__/nested-loop-non-outer-reactivity.test.ts +177 -0
- package/src/__tests__/nested-loop-plain.test.ts +39 -25
- package/src/__tests__/nested-loop-reactive-attrs.test.ts +16 -6
- package/src/__tests__/preamble-branch-arm-rerun.test.ts +135 -0
- 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/__tests__/style-css-var-reactive.test.ts +4 -1
- package/src/analyzer-context.ts +12 -10
- package/src/analyzer.ts +4 -0
- package/src/index.ts +8 -2
- package/src/ir-to-client-js/collect-elements.ts +126 -32
- 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 +12 -6
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +36 -59
- package/src/ir-to-client-js/control-flow/plan/build-lazy-row.ts +25 -4
- package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +36 -9
- 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 +116 -0
- package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +23 -8
- package/src/ir-to-client-js/control-flow/plan/inner-loop.ts +24 -45
- 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-child-arm.ts +14 -0
- 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/inner-loop.ts +20 -66
- package/src/ir-to-client-js/control-flow/stringify/insert.ts +4 -2
- package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +22 -15
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +53 -11
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +4 -2
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +29 -18
- package/src/ir-to-client-js/csr-substitute.ts +29 -0
- package/src/ir-to-client-js/emit-reactive.ts +92 -6
- package/src/ir-to-client-js/emit-registration.ts +2 -4
- package/src/ir-to-client-js/html-template.ts +67 -245
- 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/reactivity.ts +73 -27
- package/src/ir-to-client-js/safe-html.ts +243 -0
- package/src/ir-to-client-js/types.ts +19 -3
- package/src/ir-to-client-js/utils.ts +58 -10
- package/src/jsx-to-ir.ts +136 -24
- package/src/prop-rewrite.ts +8 -4
- package/src/props-binding.ts +37 -1
- package/src/reactivity-checker.ts +60 -0
- package/src/ssr-defaults.ts +45 -0
- package/src/types.ts +5 -10
- package/src/__tests__/text-slot-escaping.test.ts +0 -65
|
@@ -38,15 +38,27 @@ import { identifierCallPattern } from '../identifier-pattern.ts'
|
|
|
38
38
|
* `index` field through before this fix (see each call site's own
|
|
39
39
|
* `undefined`/omitted-arg default), so `undefined` here still means "not
|
|
40
40
|
* guarded," not "no index param."
|
|
41
|
+
*
|
|
42
|
+
* `parentScope` (#2861): stacks this loop-row frame on top of an
|
|
43
|
+
* ENCLOSING loop's own scope, rather than always starting from
|
|
44
|
+
* `BindingScope.EMPTY` — for a nested `.map()` inside another `.map()`'s
|
|
45
|
+
* JSX body. `BindingScope.lookup`/`valueBoundNames` search innermost-first
|
|
46
|
+
* across every frame, so a row expression referencing the OUTER loop's
|
|
47
|
+
* item/index (with no dependency on the INNER loop's own item/index or any
|
|
48
|
+
* signal) is still granted a slot and classified reactive, instead of
|
|
49
|
+
* silently returning `'none'` because only the inner-most frame was ever
|
|
50
|
+
* visible. Omitted (or `undefined`) for a loop with no enclosing loop —
|
|
51
|
+
* identical to the pre-#2861 shape.
|
|
41
52
|
*/
|
|
42
53
|
export function buildLoopRowScope(
|
|
43
54
|
loopParam?: string,
|
|
44
55
|
loopParamBindings?: readonly LoopParamBinding[],
|
|
45
56
|
preambleNames?: ReadonlySet<string>,
|
|
46
57
|
loopIndex?: string | null,
|
|
58
|
+
parentScope?: BindingScope,
|
|
47
59
|
): BindingScope | undefined {
|
|
48
60
|
if (!loopParam) return undefined
|
|
49
|
-
return BindingScope.EMPTY.enterLoopRow({
|
|
61
|
+
return (parentScope ?? BindingScope.EMPTY).enterLoopRow({
|
|
50
62
|
param: loopParam,
|
|
51
63
|
paramBindings: loopParamBindings,
|
|
52
64
|
index: loopIndex,
|
|
@@ -274,14 +286,28 @@ function needsEffectWrapperCore(
|
|
|
274
286
|
/**
|
|
275
287
|
* Why a given expression should be treated as reactive inside a loop item.
|
|
276
288
|
*
|
|
277
|
-
* Surfaces the
|
|
289
|
+
* Surfaces the three distinct reasons loop-child collectors care about:
|
|
278
290
|
* - `signal-or-memo-or-prop` — the expression reads a signal getter, a memo,
|
|
279
291
|
* or a prop name (what `needsEffectWrapper` already classifies).
|
|
280
|
-
* - `loop-param` — the expression reads the loop
|
|
281
|
-
* per-item signal accessor at runtime;
|
|
282
|
-
* does not know about loop params.
|
|
292
|
+
* - `loop-param` — the expression reads the loop's item (or a destructured
|
|
293
|
+
* binding of it), which becomes a per-item signal accessor at runtime;
|
|
294
|
+
* the string-level `needsEffectWrapper` does not know about loop params.
|
|
295
|
+
* - `loop-index` (#2861) — the expression reads ONLY the loop's index
|
|
296
|
+
* parameter (`.map((item, i) => ...)`'s `i`), with no item/signal/memo/prop
|
|
297
|
+
* read anywhere else in it (e.g. bare `{i}`, `class={i % 2 === 0 ? …}`).
|
|
298
|
+
* `mapArray`/`mapArrayAnchored` hand `renderItem` an index ACCESSOR
|
|
299
|
+
* exactly like the item accessor (#2859/#2860), and `mapArrayLazy` tracks
|
|
300
|
+
* a row's position on `entry.index` exactly like `entry.item` — but
|
|
301
|
+
* before this classification existed, nothing ever put such an
|
|
302
|
+
* expression in front of that machinery in the first place: Phase 1
|
|
303
|
+
* (`jsx-to-ir.ts`'s `referencesLoopParam`) already grants it a patchable
|
|
304
|
+
* slot via `BindingScope.valueBoundNames()` (item/index/destructure
|
|
305
|
+
* together), while this function — Phase 2 — had no equivalent case for
|
|
306
|
+
* the index alone, so the slot was written once at row-creation time and
|
|
307
|
+
* never revisited.
|
|
283
308
|
*
|
|
284
|
-
* `none` means
|
|
309
|
+
* `none` means none of the three applies and the collector can skip the
|
|
310
|
+
* expression.
|
|
285
311
|
*
|
|
286
312
|
* Consolidates the duplicated reactive-classification check shared by
|
|
287
313
|
* `collectLoopChildReactiveTexts`, `collectLoopChildReactiveAttrs`, and
|
|
@@ -291,37 +317,53 @@ export type ReactivitySource =
|
|
|
291
317
|
| { kind: 'none' }
|
|
292
318
|
| { kind: 'signal-or-memo-or-prop' }
|
|
293
319
|
| { kind: 'loop-param'; param: string }
|
|
320
|
+
| { kind: 'loop-index'; param: string }
|
|
294
321
|
|
|
295
322
|
/**
|
|
296
323
|
* Classify a (constant-expanded) expression as reactive inside a loop item.
|
|
297
|
-
* See `ReactivitySource` for the
|
|
298
|
-
* matching
|
|
299
|
-
* expression also reads a signal — the `kind` is purely
|
|
300
|
-
* collectors only care about `kind !== 'none'`.
|
|
324
|
+
* See `ReactivitySource` for the three reasons we care about. Loop-param
|
|
325
|
+
* and loop-index matching take precedence so one of them is reported even
|
|
326
|
+
* when the expression also reads a signal — the `kind` is purely
|
|
327
|
+
* informational and collectors only care about `kind !== 'none'`.
|
|
328
|
+
*
|
|
329
|
+
* Takes the row's `BindingScope` (#2482) rather than a raw `(loopParam,
|
|
330
|
+
* loopParamBindings)` pair so this reads the EXACT SAME query Phase 1's
|
|
331
|
+
* `referencesLoopParam` reads (`scope.valueBoundNames()` — item/index/
|
|
332
|
+
* destructure together) to decide whether an expression gets a patchable
|
|
333
|
+
* slot at all. `undefined` means no enclosing loop.
|
|
334
|
+
*
|
|
335
|
+
* For destructured `.map()` callbacks (#951), a `'destructure'`-sourced
|
|
336
|
+
* binding is reported as `loop-param` — the pattern text itself (e.g.
|
|
337
|
+
* `{ id, label }`) never word-matches on a bare binding like `id`, so
|
|
338
|
+
* iterating `valueBoundNames()` (rather than testing the raw param text)
|
|
339
|
+
* is what catches a destructured reference at all.
|
|
301
340
|
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
341
|
+
* Item/destructure bindings are checked before the index so `loop-param`
|
|
342
|
+
* wins when an expression reads both (e.g. `` `${item.id}-${i}` ``) — this
|
|
343
|
+
* only affects which `kind` is reported, since every caller of this
|
|
344
|
+
* function besides the two `ReactivitySource`-typed union members simply
|
|
345
|
+
* tests `.kind !== 'none'`.
|
|
307
346
|
*/
|
|
308
347
|
export function classifyReactivity(
|
|
309
348
|
expr: string,
|
|
310
349
|
ctx: ClientJsContext,
|
|
311
|
-
|
|
312
|
-
loopParamBindings?: readonly LoopParamBinding[],
|
|
350
|
+
scope: BindingScope | undefined,
|
|
313
351
|
freeIdentifiers?: ReadonlySet<string>,
|
|
314
352
|
): ReactivitySource {
|
|
315
353
|
const has = (name: string): boolean =>
|
|
316
354
|
freeIdentifiers ? freeIdentifiers.has(name) : tokenContainsIdent(expr, name)
|
|
317
|
-
if (
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
355
|
+
if (scope) {
|
|
356
|
+
let indexHit: string | undefined
|
|
357
|
+
for (const name of scope.valueBoundNames()) {
|
|
358
|
+
if (!has(name)) continue
|
|
359
|
+
const source = scope.lookup(name)?.binding.source
|
|
360
|
+
if (source === 'index') {
|
|
361
|
+
indexHit ??= name
|
|
362
|
+
continue
|
|
321
363
|
}
|
|
364
|
+
return { kind: 'loop-param', param: name }
|
|
322
365
|
}
|
|
323
|
-
|
|
324
|
-
return { kind: 'loop-param', param: loopParam }
|
|
366
|
+
if (indexHit) return { kind: 'loop-index', param: indexHit }
|
|
325
367
|
}
|
|
326
368
|
if (needsEffectWrapper(expr, ctx, freeIdentifiers)) {
|
|
327
369
|
return { kind: 'signal-or-memo-or-prop' }
|
|
@@ -666,9 +708,11 @@ export function collectLoopChildReactiveTexts(
|
|
|
666
708
|
stopAtReactiveConditionals = false,
|
|
667
709
|
preambleNames?: ReadonlySet<string>,
|
|
668
710
|
loopIndex?: string | null,
|
|
711
|
+
/** Enclosing loop's own scope, for a NESTED loop (#2861) — see `buildLoopRowScope`. */
|
|
712
|
+
parentScope?: BindingScope,
|
|
669
713
|
): LoopChildReactiveText[] {
|
|
670
714
|
const texts: LoopChildReactiveText[] = []
|
|
671
|
-
const scope = buildLoopRowScope(loopParam, loopParamBindings, preambleNames, loopIndex)
|
|
715
|
+
const scope = buildLoopRowScope(loopParam, loopParamBindings, preambleNames, loopIndex, parentScope)
|
|
672
716
|
walkIR(node, false, {
|
|
673
717
|
// Skip loop/async/if-statement subtrees — the original walker omitted
|
|
674
718
|
// them; they have their own scopes (inner-loop reconciliation, async
|
|
@@ -696,7 +740,7 @@ export function collectLoopChildReactiveTexts(
|
|
|
696
740
|
// `classifyReactivity` can't see through) still gets an update
|
|
697
741
|
// effect instead of silently freezing at its SSR value (#2282).
|
|
698
742
|
const reactive =
|
|
699
|
-
classifyReactivity(expanded.expr, ctx,
|
|
743
|
+
classifyReactivity(expanded.expr, ctx, scope, expanded.freeIds).kind !== 'none'
|
|
700
744
|
|| decideWrapFromAstFlags(n).wrap
|
|
701
745
|
if (!reactive) return
|
|
702
746
|
texts.push({
|
|
@@ -746,9 +790,11 @@ export function collectLoopChildReactiveAttrs(
|
|
|
746
790
|
stopAtReactiveConditionals = false,
|
|
747
791
|
preambleNames?: ReadonlySet<string>,
|
|
748
792
|
loopIndex?: string | null,
|
|
793
|
+
/** Enclosing loop's own scope, for a NESTED loop (#2861) — see `buildLoopRowScope`. */
|
|
794
|
+
parentScope?: BindingScope,
|
|
749
795
|
): LoopChildReactiveAttr[] {
|
|
750
796
|
const attrs: LoopChildReactiveAttr[] = []
|
|
751
|
-
const scope = buildLoopRowScope(loopParam, loopParamBindings, preambleNames, loopIndex)
|
|
797
|
+
const scope = buildLoopRowScope(loopParam, loopParamBindings, preambleNames, loopIndex, parentScope)
|
|
752
798
|
traverseElements(node, (el) => {
|
|
753
799
|
if (el.slotId) {
|
|
754
800
|
for (const attr of el.attrs) {
|
|
@@ -799,7 +845,7 @@ export function collectLoopChildReactiveAttrs(
|
|
|
799
845
|
preambleNames.size > 0 &&
|
|
800
846
|
anyNameIn(expanded.freeIds ?? extractFreeIdentifiersFromText(expanded.expr), preambleNames)
|
|
801
847
|
const reactive =
|
|
802
|
-
classifyReactivity(expanded.expr, ctx,
|
|
848
|
+
classifyReactivity(expanded.expr, ctx, scope, expanded.freeIds).kind !== 'none'
|
|
803
849
|
|| readsPreamble
|
|
804
850
|
|| attr.callsReactiveGetters
|
|
805
851
|
|| attr.hasFunctionCalls
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escape-by-default for client-JS HTML template strings (#2795).
|
|
3
|
+
*
|
|
4
|
+
* Every builder in `html-template.ts` assembles a JS template literal whose
|
|
5
|
+
* `${...}` holes are evaluated in the browser and parsed as HTML
|
|
6
|
+
* (`innerHTML`). Whether a hole's value is escaped used to be an OPT-IN each
|
|
7
|
+
* builder remembered separately — `escapeInClientTemplate` on the IR node, a
|
|
8
|
+
* pre-baked `escapeText(...)` string in `templateExpr`, an
|
|
9
|
+
* `escapeLeafTextExpressions` rewrite pass, and a hand-rolled `escapeText`
|
|
10
|
+
* in `buildPreambleRegionPlans` — and a forgotten opt-in shipped raw text
|
|
11
|
+
* twice (#1694, #2765/#2792). This module inverts that:
|
|
12
|
+
*
|
|
13
|
+
* - `SafeHtml` is a compile-time nominal brand (the `TsxSourceText`
|
|
14
|
+
* precedent, `types.ts`) on a JS EXPRESSION STRING meaning "when this
|
|
15
|
+
* evaluates in the client template, the result is HTML that is safe to
|
|
16
|
+
* concatenate in". Nothing outside this module can construct one, so a
|
|
17
|
+
* `SafeHtml` value is the compiler vouching for itself — unlike the
|
|
18
|
+
* runtime `bfMarkup()` brand (`@barefootjs/client/runtime`), which
|
|
19
|
+
* vouches for a value the compiler never saw.
|
|
20
|
+
* - `interp` is the ONLY way to turn a value into a `${...}` hole, and it
|
|
21
|
+
* accepts `SafeHtml` only. A builder holding a plain `string` cannot
|
|
22
|
+
* splice it: it must either go through a named producer below (each of
|
|
23
|
+
* which documents WHY its output is safe) or through `spliceChildValue`,
|
|
24
|
+
* the door for a child-position expression, whose default is
|
|
25
|
+
* `escapeText(...)`.
|
|
26
|
+
*
|
|
27
|
+
* The runtime dispatch that already existed for values the compiler cannot
|
|
28
|
+
* classify statically is unchanged: a branch-slot value still goes through
|
|
29
|
+
* `__bfSlot` (may be a live Node), a claim-plan `'markup'` slot still goes
|
|
30
|
+
* through `escapeTextOrMarkup` (may be a `bfMarkup()`-branded prop). Those
|
|
31
|
+
* are producers here too — they escape internally, so their output is safe
|
|
32
|
+
* by construction.
|
|
33
|
+
*
|
|
34
|
+
* Attribute-value holes (`escapeAttr`, `spreadAttrs`) are a separate,
|
|
35
|
+
* always-on path in `html-template.ts` and are not routed through this
|
|
36
|
+
* module; `client-template-escape-soundness.test.ts` pins their count.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import type { IRExpression } from '../types.ts'
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A JS expression string that evaluates, in the client template, to HTML
|
|
43
|
+
* safe to splice raw. Branded at construction by the producers below; only
|
|
44
|
+
* `interp` consumes it.
|
|
45
|
+
*/
|
|
46
|
+
export type SafeHtml = string & { readonly __safeHtmlBrand: unique symbol }
|
|
47
|
+
|
|
48
|
+
/** The single place the brand is applied. Module-private on purpose. */
|
|
49
|
+
function safeHtml(expr: string): SafeHtml {
|
|
50
|
+
return expr as SafeHtml
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Emit a `${...}` hole. The parameter type is the whole mechanism: a builder
|
|
55
|
+
* that has only a `string` in hand cannot call this, so "forgot to escape"
|
|
56
|
+
* is a type error at the splice, not a runtime surprise in the browser.
|
|
57
|
+
*/
|
|
58
|
+
export function interp(span: SafeHtml): string {
|
|
59
|
+
return `\${${span}}`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Producers — escaping forms. Each wraps a runtime helper from
|
|
64
|
+
// `@barefootjs/client/runtime` (see `imports.ts`'s allowlist) that escapes
|
|
65
|
+
// its own input.
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
/** `escapeText(expr)` — the default for any child-position value. */
|
|
69
|
+
export function escapedText(expr: string): SafeHtml {
|
|
70
|
+
return safeHtml(`escapeText(${expr})`)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* `escapeTextOrMarkup(expr)` — `escapeText`'s strict superset that unwraps a
|
|
75
|
+
* `bfMarkup()`-branded value raw (#2651). Used only for a slot whose
|
|
76
|
+
* REACTIVE claim writer is `kind: 'markup'` (`markup-slots.ts`), so the
|
|
77
|
+
* initial render and the update agree on whether the slot may carry a
|
|
78
|
+
* compiler-built JSX prop value.
|
|
79
|
+
*/
|
|
80
|
+
export function escapedTextOrMarkup(expr: string): SafeHtml {
|
|
81
|
+
return safeHtml(`escapeTextOrMarkup(${expr})`)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* `__bfSlot(expr, slots)` — a conditional-branch `template()` value. The
|
|
86
|
+
* runtime returns raw `<!--bf-slot:N-->` markers for live Nodes (spliced
|
|
87
|
+
* back by `insert()`) and `escapeText`s every string itself
|
|
88
|
+
* (`branch-slot.ts`). Wrapping this in another escape corrupts the markers
|
|
89
|
+
* and drops slotted content (the #1694 regression) — hence a producer, not
|
|
90
|
+
* an input to `escapedText`.
|
|
91
|
+
*/
|
|
92
|
+
export function branchSlotValue(expr: string, slotsVar: string): SafeHtml {
|
|
93
|
+
return safeHtml(`__bfSlot(${expr}, ${slotsVar})`)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Producers — already-HTML forms. The value is markup the compiler or
|
|
98
|
+
// runtime built (and escaped piecewise) BEFORE it reaches the hole.
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* `markupOrEmpty(expr)` — a bare `{children}` passthrough (#2775). The value
|
|
103
|
+
* is the HTML string `materializeComponent` joined from the caller's
|
|
104
|
+
* children, or `undefined` when none were passed; the helper's entire job is
|
|
105
|
+
* the nullish case. Never escaped: escaping would render real child markup
|
|
106
|
+
* as visible `<span>` text.
|
|
107
|
+
*/
|
|
108
|
+
export function childrenMarkup(expr: string): SafeHtml {
|
|
109
|
+
return safeHtml(`markupOrEmpty(${expr})`)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Stage 3 / D4 — an element-array child (`{out}`) built by an arbitrary
|
|
114
|
+
* `.map()` preamble is an array of compiled-leaf HTML strings; join it
|
|
115
|
+
* rather than let `${[...]}` `String`-comma-collapse it. Each leaf's own
|
|
116
|
+
* holes already went through `interp`, so the concatenation is exactly as
|
|
117
|
+
* safe as any other compiler-emitted fragment. Also used by
|
|
118
|
+
* `buildPreambleRegionPlans` for the region-patch effect's value so the
|
|
119
|
+
* re-render matches the row template byte-for-byte.
|
|
120
|
+
*/
|
|
121
|
+
export function joinedMarkup(expr: string): SafeHtml {
|
|
122
|
+
return safeHtml(`Array.isArray(${expr}) ? ${expr}.join('') : (${expr} ?? '')`)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* `renderChild('Name', props, ...scopeArgs)` — a child component rendered
|
|
127
|
+
* from its registered template at runtime. Its output is that component's
|
|
128
|
+
* own compiled template, whose holes went through this module in turn.
|
|
129
|
+
*/
|
|
130
|
+
export function renderChildCall(registryName: string, propsExpr: string, tailArgs: string): SafeHtml {
|
|
131
|
+
return safeHtml(`renderChild('${registryName}', ${propsExpr}${tailArgs})`)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* `dangerouslySetInnerHTML={{ __html: E }}` — the intentional, React-style,
|
|
136
|
+
* author-facing escape hatch, and the ONLY one: the element's content is
|
|
137
|
+
* whatever `E.__html` holds, unescaped by design (mirroring every SSR
|
|
138
|
+
* adapter's native handling). The name carries the warning so an audit of
|
|
139
|
+
* raw-markup producers (`grep dangerousInnerHtml`) finds it.
|
|
140
|
+
*/
|
|
141
|
+
export function dangerousInnerHtml(expr: string): SafeHtml {
|
|
142
|
+
return safeHtml(`((${expr}) ?? {}).__html ?? ''`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* `cond ? \`whenTrue\` : \`whenFalse\`` — both branches are compiled
|
|
147
|
+
* template-literal BODIES (already-assembled HTML with `interp`ed holes),
|
|
148
|
+
* not values; the condition only selects between them.
|
|
149
|
+
*/
|
|
150
|
+
export function conditionalMarkup(condition: string, whenTrue: string, whenFalse: string): SafeHtml {
|
|
151
|
+
return safeHtml(`${condition} ? \`${whenTrue}\` : \`${whenFalse}\``)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* `array.method(params => body).join('')` — a loop rendered inline. `body`
|
|
156
|
+
* is either a compiled row template literal (\`...\`), a preamble block
|
|
157
|
+
* returning one, or a flatMap descriptor body rendered through
|
|
158
|
+
* `renderPreamble`; every hole inside it went through `interp`.
|
|
159
|
+
*/
|
|
160
|
+
export function mappedRowsMarkup(arrayExpr: string, method: string, params: string, body: string): SafeHtml {
|
|
161
|
+
return safeHtml(`${arrayExpr}.${method}(${params} => ${body}).join('')`)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** The empty string literal — a deferred placeholder the init effect fills (#1128). */
|
|
165
|
+
export const EMPTY_MARKUP: SafeHtml = safeHtml("''")
|
|
166
|
+
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// The door for a child-position expression node.
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Recognizes a JSX child-position expression that is exactly a reference to
|
|
173
|
+
* the reserved `children` prop — bare `children` (destructured) or
|
|
174
|
+
* `<receiver>.children` for any single-identifier receiver (`props.children`,
|
|
175
|
+
* a custom props-param name, a loop-scoped alias closing over props, ...).
|
|
176
|
+
*
|
|
177
|
+
* Deliberately LOOSER than `isTransparentFragment` (`jsx-to-ir.ts`), which
|
|
178
|
+
* runs on the TS AST with the analyzer-resolved props name in hand. This
|
|
179
|
+
* layer works on IR text with no analyzer, so any single-identifier receiver
|
|
180
|
+
* is the available approximation. The looseness costs nothing measurable: an
|
|
181
|
+
* unrelated `.children` member (a tree node's own `children` array, say) is a
|
|
182
|
+
* reactive member expression that gets a `slotId` and takes the slotted arm
|
|
183
|
+
* of `spliceChildValue` before this predicate is consulted.
|
|
184
|
+
*/
|
|
185
|
+
function isChildrenPassthroughExpr(expr: string): boolean {
|
|
186
|
+
return /^([A-Za-z_$][\w$]*\.)?children$/.test(expr.trim())
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface ChildSpliceContext {
|
|
190
|
+
/**
|
|
191
|
+
* Set while emitting inside a conditional-branch `template()` arrow
|
|
192
|
+
* (`irToHtmlTemplate`'s `branchSlotsVar`): every value in that context is
|
|
193
|
+
* routed through `__bfSlot`, which owns its own coercion.
|
|
194
|
+
*/
|
|
195
|
+
readonly branchSlotsVar?: string
|
|
196
|
+
/**
|
|
197
|
+
* Slot ids whose reactive claim writer is `kind: 'markup'`
|
|
198
|
+
* (`markupSlotIdsOf(ctx)`, `markup-slots.ts`). Absent for the loop-row /
|
|
199
|
+
* branch-row builders, whose reactive twin is a `kind: 'text'` writer
|
|
200
|
+
* (`String(...)` into `nodeValue`) — plain `escapeText` is the consistent
|
|
201
|
+
* choice there, and it is also the choice that never unwraps a
|
|
202
|
+
* `bfMarkup()`-shaped object arriving in JSON-sourced loop data.
|
|
203
|
+
*/
|
|
204
|
+
readonly markupSlotIds?: ReadonlySet<string>
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* THE door for a child-position `IRExpression` value entering a client HTML
|
|
209
|
+
* template. Takes the caller's already-wrapped/transformed expression text
|
|
210
|
+
* (`wrapExpr` / `transformExpr` output) and decides, in exactly one place,
|
|
211
|
+
* which producer it goes through. The last arm is the default, and it
|
|
212
|
+
* escapes: there is no arm that returns `valueExpr` unwrapped.
|
|
213
|
+
*
|
|
214
|
+
* Arms, first match wins:
|
|
215
|
+
* 1. `joinArrayChild` → `joinedMarkup` (array of compiled leaves)
|
|
216
|
+
* 2. `branchSlotsVar` → `branchSlotValue` (runtime coercion, may be a Node)
|
|
217
|
+
* 3. `slotId` ∈ markupSlotIds → `escapedTextOrMarkup`
|
|
218
|
+
* 4. `slotId` → `escapedText`
|
|
219
|
+
* 5. `{children}` passthrough → `childrenMarkup` (bare splice only, #2775/#2786)
|
|
220
|
+
* 6. otherwise → `escapedText`
|
|
221
|
+
*
|
|
222
|
+
* Arm 5 tests BOTH `node.expr` (the original source text, stable across
|
|
223
|
+
* builders) and the caller's resolved form (parens stripped): a
|
|
224
|
+
* destructured-and-renamed children (`const { children: kids } = props`)
|
|
225
|
+
* reads `kids` in the source and `(_p.children)` after substitution, and
|
|
226
|
+
* either alone misses one of the two shapes (#2786).
|
|
227
|
+
*/
|
|
228
|
+
export function spliceChildValue(
|
|
229
|
+
node: Pick<IRExpression, 'expr' | 'slotId' | 'joinArrayChild'>,
|
|
230
|
+
valueExpr: string,
|
|
231
|
+
cx: ChildSpliceContext,
|
|
232
|
+
): SafeHtml {
|
|
233
|
+
if (node.joinArrayChild) return joinedMarkup(valueExpr)
|
|
234
|
+
if (cx.branchSlotsVar) return branchSlotValue(valueExpr, cx.branchSlotsVar)
|
|
235
|
+
if (node.slotId) {
|
|
236
|
+
return cx.markupSlotIds?.has(node.slotId) ? escapedTextOrMarkup(valueExpr) : escapedText(valueExpr)
|
|
237
|
+
}
|
|
238
|
+
const resolved = valueExpr.trim().replace(/^\(+|\)+$/g, '')
|
|
239
|
+
if (isChildrenPassthroughExpr(node.expr) || isChildrenPassthroughExpr(resolved)) {
|
|
240
|
+
return childrenMarkup(valueExpr)
|
|
241
|
+
}
|
|
242
|
+
return escapedText(valueExpr)
|
|
243
|
+
}
|
|
@@ -400,7 +400,19 @@ export interface LoopChildRef {
|
|
|
400
400
|
export interface BranchLoop extends LoopCore {
|
|
401
401
|
kind: 'branch'
|
|
402
402
|
index: string | null // Index parameter (e.g., 'i')
|
|
403
|
-
template: string // HTML template for each item
|
|
403
|
+
template: string // HTML template for each item, item-only wrapped (no index — see `templateIndexed`)
|
|
404
|
+
/**
|
|
405
|
+
* Same render as `template`, but with `index` also wrapped as an accessor
|
|
406
|
+
* (`i()`) — rendered structurally at IR time (`irToHtmlTemplate`'s
|
|
407
|
+
* `loopParams`), never via a post-hoc regex pass over `template` (#2868:
|
|
408
|
+
* a word-boundary regex over assembled HTML can match a bare tag name
|
|
409
|
+
* colliding with the index identifier, e.g. `<i>` -> `<i()>`). Present
|
|
410
|
+
* iff the loop declares an index; `build-plain-row.ts` picks this over
|
|
411
|
+
* `template` once lazy eligibility rules out the lazy row plan (which
|
|
412
|
+
* needs the index UNWRAPPED — `mapArrayLazy`'s `createRow` hands it a
|
|
413
|
+
* plain number, never an accessor).
|
|
414
|
+
*/
|
|
415
|
+
templateIndexed?: string
|
|
404
416
|
containerSlotId: string // bf slot ID of the container element (e.g., 's1' for <ul bf="s1">)
|
|
405
417
|
preamble?: MapCallbackPreamble
|
|
406
418
|
// Composite loop fields (loops whose body contains child components)
|
|
@@ -489,8 +501,6 @@ export interface NestedLoop extends LoopCore {
|
|
|
489
501
|
* (and any subsequent reads) are in scope (#1052).
|
|
490
502
|
*/
|
|
491
503
|
preamble?: MapCallbackPreamble
|
|
492
|
-
/** Whether the inner array references the outer loop param (needs reactive mapArray) */
|
|
493
|
-
refsOuterParam?: boolean
|
|
494
504
|
/** Child components inside inner loop items (for initChild/createComponent) */
|
|
495
505
|
childComponents?: import('../types.ts').IRLoopChildComponent[]
|
|
496
506
|
/** True when this loop is inside a conditional branch (handled by insert() bindEvents instead) */
|
|
@@ -622,6 +632,12 @@ export interface TopLevelLoop extends LoopCore {
|
|
|
622
632
|
slotId: string
|
|
623
633
|
index: string | null
|
|
624
634
|
template: string
|
|
635
|
+
/**
|
|
636
|
+
* Same render as `template`, but with `index` also wrapped as an accessor
|
|
637
|
+
* (`i()`) — see `BranchLoop.templateIndexed` for why this exists as a
|
|
638
|
+
* second structured render rather than a post-hoc regex pass (#2868).
|
|
639
|
+
*/
|
|
640
|
+
templateIndexed?: string
|
|
625
641
|
/**
|
|
626
642
|
* Per-iteration HTML template for static-array loops that need to
|
|
627
643
|
* self-heal on CSR mount (#1247). Unlike `template`, this variant skips
|
|
@@ -581,17 +581,63 @@ function renderLoopBindingAccess(b: LoopParamBinding, base: string): string {
|
|
|
581
581
|
* binding name is rewritten to `__bfItem()${path}` instead of wrapping the
|
|
582
582
|
* raw pattern text. `paramName` is ignored in that case — destructured
|
|
583
583
|
* callbacks never expose the pattern itself as a local.
|
|
584
|
-
|
|
585
|
-
|
|
584
|
+
*
|
|
585
|
+
* `indexParam` (#2859): the same `.map()` callback's index parameter
|
|
586
|
+
* (`(item, i) => ...`'s `i`), when the loop declares one. `mapArray`/
|
|
587
|
+
* `mapArrayAnchored` hand `renderItem` an INDEX ACCESSOR (mirroring the item
|
|
588
|
+
* accessor) precisely so a same-key reorder — which never re-invokes
|
|
589
|
+
* `renderItem`, only pushes fresh values through the item/index signals —
|
|
590
|
+
* can keep a row's index-derived output live. A bare reference to
|
|
591
|
+
* `indexParam` is therefore rewritten the same way as the item param; a
|
|
592
|
+
* caller that never threads an index through (e.g. no second callback
|
|
593
|
+
* param) simply omits it and behavior is unchanged. Rewritten as a SEPARATE
|
|
594
|
+
* pass after the item/bindings rewrite so an index name that happens to
|
|
595
|
+
* collide with a destructured binding name is a no-op here (the binding
|
|
596
|
+
* rewrite already consumed it; `!== paramName` below only guards the plain,
|
|
597
|
+
* non-destructured case, since `bindings` and `paramName` are mutually
|
|
598
|
+
* exclusive inputs to the item rewrite above).
|
|
599
|
+
*/
|
|
600
|
+
export function wrapLoopParamAsAccessor(
|
|
601
|
+
expr: string,
|
|
602
|
+
paramName: string,
|
|
603
|
+
bindings?: readonly LoopParamBinding[],
|
|
604
|
+
indexParam?: string | null,
|
|
605
|
+
): string {
|
|
606
|
+
let result: string
|
|
586
607
|
if (bindings && bindings.length > 0) {
|
|
587
|
-
|
|
608
|
+
result = rewriteLoopBindingRefs(expr, bindings, '__bfItem()')
|
|
609
|
+
} else {
|
|
610
|
+
// `paramName` is a legal JS identifier and may itself start with `$`
|
|
611
|
+
// (`$1`, `$&`, …) — a plain string replacement would risk `String.replace`
|
|
612
|
+
// reading those as backreference/whole-match sequences, so use a replacer
|
|
613
|
+
// function to insert the accessor text literally (#2592).
|
|
614
|
+
const re = new RegExp(`${ID_BOUNDARY_BEFORE}${escapeIdentifierForRegex(paramName)}(?!\\s*\\()(?!-)${ID_BOUNDARY_AFTER}`, 'gu')
|
|
615
|
+
result = replaceInExprContexts(expr, re, () => `${paramName}()`)
|
|
616
|
+
}
|
|
617
|
+
if (indexParam && indexParam !== paramName) {
|
|
618
|
+
result = wrapIndexParamAsAccessor(result, indexParam)
|
|
588
619
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
620
|
+
return result
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* The index-only half of `wrapLoopParamAsAccessor` — same regex shape, own
|
|
625
|
+
* identifier. Factored out so `wrapLoopParamAsAccessor` can run it as a
|
|
626
|
+
* second pass regardless of which item-rewrite branch (bindings vs. plain
|
|
627
|
+
* param) ran first (#2859).
|
|
628
|
+
*
|
|
629
|
+
* Also exported standalone for `build-loop.ts`/`build-branch-loop.ts`: a
|
|
630
|
+
* plain loop's `template`/`mapPreambleWrapped` are built ONCE, before lazy
|
|
631
|
+
* eligibility is known, because the lazy row plan reuses them verbatim when
|
|
632
|
+
* this loop turns out to be lazy-eligible (`mapArrayLazy`'s `createRow`
|
|
633
|
+
* still hands the row a plain index NUMBER, never an accessor). So those two
|
|
634
|
+
* builders wrap item references up front (safe either way) and apply this
|
|
635
|
+
* index-only pass afterward, ONLY once `buildLazyRowPlan` confirms the loop
|
|
636
|
+
* is NOT going lazy.
|
|
637
|
+
*/
|
|
638
|
+
export function wrapIndexParamAsAccessor(expr: string, indexParam: string): string {
|
|
639
|
+
const re = new RegExp(`${ID_BOUNDARY_BEFORE}${escapeIdentifierForRegex(indexParam)}(?!\\s*\\()(?!-)${ID_BOUNDARY_AFTER}`, 'gu')
|
|
640
|
+
return replaceInExprContexts(expr, re, () => `${indexParam}()`)
|
|
595
641
|
}
|
|
596
642
|
|
|
597
643
|
/**
|
|
@@ -718,6 +764,8 @@ export function substituteLoopBindings(
|
|
|
718
764
|
export interface LoopParamSpec {
|
|
719
765
|
param: string
|
|
720
766
|
bindings?: readonly LoopParamBinding[]
|
|
767
|
+
/** This loop's index parameter name, when it declares one (#2859). */
|
|
768
|
+
index?: string | null
|
|
721
769
|
}
|
|
722
770
|
|
|
723
771
|
/**
|
|
@@ -747,7 +795,7 @@ export function wrapExprWithLoopParams(expr: string, loopParams?: ReadonlyArray<
|
|
|
747
795
|
let result = expr
|
|
748
796
|
for (const p of loopParams) {
|
|
749
797
|
const spec = typeof p === 'string' ? { param: p } : p
|
|
750
|
-
result = wrapLoopParamAsAccessor(result, spec.param, spec.bindings)
|
|
798
|
+
result = wrapLoopParamAsAccessor(result, spec.param, spec.bindings, spec.index)
|
|
751
799
|
}
|
|
752
800
|
return result
|
|
753
801
|
}
|