@barefootjs/jsx 0.10.1 → 0.11.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.
Files changed (137) hide show
  1. package/dist/compiler.d.ts.map +1 -1
  2. package/dist/debug-profile.d.ts +115 -0
  3. package/dist/debug-profile.d.ts.map +1 -0
  4. package/dist/debug.d.ts +4 -3
  5. package/dist/debug.d.ts.map +1 -1
  6. package/dist/expression-parser.d.ts +31 -0
  7. package/dist/expression-parser.d.ts.map +1 -1
  8. package/dist/index.d.ts +8 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +1872 -207
  11. package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts +6 -0
  12. package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts.map +1 -1
  13. package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.d.ts +1 -1
  14. package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.d.ts.map +1 -1
  15. package/dist/ir-to-client-js/control-flow/plan/build-component-loop.d.ts +1 -1
  16. package/dist/ir-to-client-js/control-flow/plan/build-component-loop.d.ts.map +1 -1
  17. package/dist/ir-to-client-js/control-flow/plan/build-composite-loop.d.ts +2 -2
  18. package/dist/ir-to-client-js/control-flow/plan/build-composite-loop.d.ts.map +1 -1
  19. package/dist/ir-to-client-js/control-flow/plan/build-event-delegation.d.ts +3 -3
  20. package/dist/ir-to-client-js/control-flow/plan/build-event-delegation.d.ts.map +1 -1
  21. package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts.map +1 -1
  22. package/dist/ir-to-client-js/control-flow/plan/build-insert.d.ts +2 -0
  23. package/dist/ir-to-client-js/control-flow/plan/build-insert.d.ts.map +1 -1
  24. package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts +2 -0
  25. package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts.map +1 -1
  26. package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts +4 -2
  27. package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
  28. package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts +3 -1
  29. package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts.map +1 -1
  30. package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts +7 -0
  31. package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts.map +1 -1
  32. package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts +7 -0
  33. package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts.map +1 -1
  34. package/dist/ir-to-client-js/control-flow/plan/insert.d.ts +8 -0
  35. package/dist/ir-to-client-js/control-flow/plan/insert.d.ts.map +1 -1
  36. package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts +8 -0
  37. package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts.map +1 -1
  38. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +28 -0
  39. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
  40. package/dist/ir-to-client-js/control-flow/plan/reactive-effects.d.ts +7 -0
  41. package/dist/ir-to-client-js/control-flow/plan/reactive-effects.d.ts.map +1 -1
  42. package/dist/ir-to-client-js/control-flow/stringify/component-loop.d.ts.map +1 -1
  43. package/dist/ir-to-client-js/control-flow/stringify/composite-loop.d.ts.map +1 -1
  44. package/dist/ir-to-client-js/control-flow/stringify/event-delegation.d.ts.map +1 -1
  45. package/dist/ir-to-client-js/control-flow/stringify/event-listener.d.ts +1 -1
  46. package/dist/ir-to-client-js/control-flow/stringify/event-listener.d.ts.map +1 -1
  47. package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts +1 -1
  48. package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts.map +1 -1
  49. package/dist/ir-to-client-js/control-flow/stringify/insert.d.ts.map +1 -1
  50. package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts +2 -2
  51. package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
  52. package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
  53. package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
  54. package/dist/ir-to-client-js/control-flow.d.ts.map +1 -1
  55. package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
  56. package/dist/ir-to-client-js/imports.d.ts +2 -2
  57. package/dist/ir-to-client-js/imports.d.ts.map +1 -1
  58. package/dist/ir-to-client-js/index.d.ts +2 -2
  59. package/dist/ir-to-client-js/index.d.ts.map +1 -1
  60. package/dist/ir-to-client-js/phases/effects-and-on-mounts.d.ts.map +1 -1
  61. package/dist/ir-to-client-js/phases/event-handlers.d.ts.map +1 -1
  62. package/dist/ir-to-client-js/plan/build-declaration-emit.d.ts.map +1 -1
  63. package/dist/ir-to-client-js/plan/declaration-emit.d.ts +6 -0
  64. package/dist/ir-to-client-js/plan/declaration-emit.d.ts.map +1 -1
  65. package/dist/ir-to-client-js/types.d.ts +5 -0
  66. package/dist/ir-to-client-js/types.d.ts.map +1 -1
  67. package/dist/ir-to-client-js/utils.d.ts +29 -0
  68. package/dist/ir-to-client-js/utils.d.ts.map +1 -1
  69. package/dist/loop-destructure.d.ts +26 -0
  70. package/dist/loop-destructure.d.ts.map +1 -0
  71. package/dist/profiler.d.ts +492 -0
  72. package/dist/profiler.d.ts.map +1 -0
  73. package/dist/types.d.ts +8 -0
  74. package/dist/types.d.ts.map +1 -1
  75. package/package.json +2 -2
  76. package/src/__tests__/debug-profile.test.ts +405 -0
  77. package/src/__tests__/expression-parser.test.ts +44 -1
  78. package/src/__tests__/profile-bfid-emission.test.ts +63 -0
  79. package/src/__tests__/profile-binding-ids.test.ts +123 -0
  80. package/src/__tests__/profile-cond-binding-ids.test.ts +80 -0
  81. package/src/__tests__/profile-loop-binding-ids.test.ts +106 -0
  82. package/src/__tests__/profile-nested-binding-ids.test.ts +153 -0
  83. package/src/__tests__/profile-turn-markers-branch.test.ts +83 -0
  84. package/src/__tests__/profile-turn-markers-delegation.test.ts +63 -0
  85. package/src/__tests__/profile-turn-markers.test.ts +54 -0
  86. package/src/__tests__/profiler-batch-advisor.test.ts +198 -0
  87. package/src/__tests__/profiler-coverage-conformance.test.ts +360 -0
  88. package/src/__tests__/profiler-e2e.test.ts +104 -0
  89. package/src/__tests__/profiler-hot-subscribers.test.ts +263 -0
  90. package/src/__tests__/profiler-wasted-re-runs.test.ts +147 -0
  91. package/src/__tests__/profiler.test.ts +408 -0
  92. package/src/compiler.ts +3 -0
  93. package/src/debug-profile.ts +543 -0
  94. package/src/debug.ts +192 -28
  95. package/src/expression-parser.ts +53 -0
  96. package/src/index.ts +72 -1
  97. package/src/ir-to-client-js/control-flow/plan/branch-loop.ts +6 -0
  98. package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +5 -3
  99. package/src/ir-to-client-js/control-flow/plan/build-component-loop.ts +3 -1
  100. package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +8 -2
  101. package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +19 -3
  102. package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +2 -0
  103. package/src/ir-to-client-js/control-flow/plan/build-insert.ts +9 -2
  104. package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +9 -1
  105. package/src/ir-to-client-js/control-flow/plan/build-loop.ts +12 -8
  106. package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +10 -4
  107. package/src/ir-to-client-js/control-flow/plan/event-delegation.ts +7 -0
  108. package/src/ir-to-client-js/control-flow/plan/inner-loop.ts +7 -0
  109. package/src/ir-to-client-js/control-flow/plan/insert.ts +8 -0
  110. package/src/ir-to-client-js/control-flow/plan/loop-child-arm.ts +8 -0
  111. package/src/ir-to-client-js/control-flow/plan/loop.ts +28 -0
  112. package/src/ir-to-client-js/control-flow/plan/reactive-effects.ts +7 -0
  113. package/src/ir-to-client-js/control-flow/stringify/branch-loop.ts +5 -3
  114. package/src/ir-to-client-js/control-flow/stringify/component-loop.ts +4 -2
  115. package/src/ir-to-client-js/control-flow/stringify/composite-loop.ts +6 -3
  116. package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +14 -2
  117. package/src/ir-to-client-js/control-flow/stringify/event-listener.ts +5 -2
  118. package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +13 -11
  119. package/src/ir-to-client-js/control-flow/stringify/insert.ts +19 -7
  120. package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +18 -13
  121. package/src/ir-to-client-js/control-flow/stringify/loop.ts +9 -7
  122. package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +18 -14
  123. package/src/ir-to-client-js/control-flow.ts +12 -6
  124. package/src/ir-to-client-js/emit-reactive.ts +18 -5
  125. package/src/ir-to-client-js/imports.ts +2 -0
  126. package/src/ir-to-client-js/index.ts +6 -1
  127. package/src/ir-to-client-js/phases/effects-and-on-mounts.ts +10 -4
  128. package/src/ir-to-client-js/phases/event-handlers.ts +6 -2
  129. package/src/ir-to-client-js/plan/build-declaration-emit.ts +7 -1
  130. package/src/ir-to-client-js/plan/declaration-emit.ts +6 -0
  131. package/src/ir-to-client-js/stringify/declaration-emit.ts +12 -6
  132. package/src/ir-to-client-js/types.ts +5 -0
  133. package/src/ir-to-client-js/utils.ts +37 -0
  134. package/src/jsx-to-ir.ts +2 -2
  135. package/src/loop-destructure.ts +170 -0
  136. package/src/profiler.ts +1488 -0
  137. package/src/types.ts +8 -0
package/src/debug.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  * paths, and component reactive structure — all without running any code.
6
6
  */
7
7
 
8
+ import ts from 'typescript'
8
9
  import type {
9
10
  ComponentIR,
10
11
  IRNode,
@@ -27,6 +28,7 @@ import { buildMetadata } from './compiler.ts'
27
28
  import { analyzeClientNeeds } from './ir-to-client-js/index.ts'
28
29
  import type { WrapReason } from './ir-to-client-js/reactivity.ts'
29
30
  import { decideWrapFromAstFlags } from './ir-to-client-js/reactivity.ts'
31
+ import { tokenContainsIdent } from './ir-to-client-js/utils.ts'
30
32
 
31
33
  // =============================================================================
32
34
  // Types
@@ -274,7 +276,12 @@ export function buildComponentGraph(source: string, filePath: string, componentN
274
276
  errors: [],
275
277
  }
276
278
 
277
- return buildGraphFromIR(componentIR)
279
+ const graph = buildGraphFromIR(componentIR)
280
+ // `findSourceFile` extracts the path from signal/memo/effect metadata; for
281
+ // components with no reactive state it returns '' because there are no
282
+ // located nodes. Fall back to the caller-supplied filePath so callers
283
+ // always get a non-empty sourceFile. (#1690 Bug A)
284
+ return graph.sourceFile ? graph : { ...graph, sourceFile: filePath }
278
285
  }
279
286
 
280
287
  /**
@@ -286,9 +293,73 @@ export function buildGraphFromIR(ir: ComponentIR): ComponentGraph {
286
293
  const memoNames = new Set(meta.memos.map(m => m.name))
287
294
  const signalSetters = new Map(meta.signals.filter(s => s.setter).map(s => [s.setter!, s.getter]))
288
295
 
296
+ // Does an attribute expression read a component prop? Mirrors the emitter's
297
+ // `needsEffectWrapper` prop gate (`reactivity.ts`):
298
+ // - any individual destructured prop name (`{ className }` → `class={className}`),
299
+ // - or a `<propsObject>.x` member access (`class={props.className}`),
300
+ // both excluding `children` (server-rendered, never wrapped). Detection is
301
+ // structural, not a raw-string regex: destructured names use the IR's
302
+ // lexer-resolved `freeIdentifiers` (falling back to the lexer-aware
303
+ // `tokenContainsIdent`), and the props-object case parses the expression and
304
+ // walks for a real `props.member` access — so a `props.` inside a string
305
+ // literal / comment can't false-match, and bare `props` (`id={props}`) or
306
+ // `props.children` are correctly NOT treated as reactive (matching the
307
+ // emitter, which only wraps `<propsObject>.<non-children>`).
308
+ //
309
+ // Prop-driven attribute bindings are wrapped in a `createEffect` by the
310
+ // emitter (and so emit a `#binding:<slot>` profiler id), but the debug-side
311
+ // collector only tracked signal/memo deps before — so those ids resolved to
312
+ // `(unresolved)` in `bf debug profile` (#1844 follow-up). Detecting prop reads
313
+ // here closes that emit↔analyzer gap.
314
+ const destructuredPropNames = new Set(meta.propsParams.map(p => p.name).filter(n => n !== 'children'))
315
+ const propsObjectName = meta.propsObjectName
316
+
317
+ // A binding often reads a prop *indirectly* through a local const:
318
+ // `const classes = `…${variant}…${size}…${className}``, then
319
+ // `<button class={classes}>` / `<Slot className={classes}>`. The emitter
320
+ // inlines `classes`, sees the prop reads, and wraps the binding in a
321
+ // `createEffect` (emitting `#binding:<slot>`) — but the expression's only free
322
+ // identifier is the local `classes`, so the direct prop check above misses it
323
+ // and the id resolves to `(unresolved)` (the Slot-composed-button case, #1863).
324
+ // Precompute every local const whose value transitively derives from a prop,
325
+ // so reading such a const counts as reading a prop. Module-level consts can't
326
+ // reach component props, so they fall out naturally.
327
+ const constByName = new Map(meta.localConstants.map(c => [c.name, c]))
328
+ const propDerivedConsts = new Set<string>()
329
+ {
330
+ const onStack = new Set<string>()
331
+ const derivesFromProp = (name: string): boolean => {
332
+ if (propDerivedConsts.has(name)) return true
333
+ const c = constByName.get(name)
334
+ if (!c?.freeIdentifiers || onStack.has(name)) return false
335
+ onStack.add(name)
336
+ let derived = false
337
+ for (const free of c.freeIdentifiers) {
338
+ if (destructuredPropNames.has(free) || free === propsObjectName || derivesFromProp(free)) {
339
+ derived = true
340
+ break
341
+ }
342
+ }
343
+ onStack.delete(name)
344
+ if (derived) propDerivedConsts.add(name)
345
+ return derived
346
+ }
347
+ for (const c of meta.localConstants) derivesFromProp(c.name)
348
+ }
349
+
350
+ const exprReadsProp = (expr: string, freeIds?: ReadonlySet<string>): boolean => {
351
+ for (const name of destructuredPropNames) {
352
+ if (freeIds ? freeIds.has(name) : tokenContainsIdent(expr, name)) return true
353
+ }
354
+ for (const name of propDerivedConsts) {
355
+ if (freeIds ? freeIds.has(name) : tokenContainsIdent(expr, name)) return true
356
+ }
357
+ return propsObjectName ? exprReadsPropMember(expr, propsObjectName) : false
358
+ }
359
+
289
360
  // Collect DOM bindings from IR tree
290
361
  const domBindings: DomBinding[] = []
291
- collectDomBindings(ir.root, domBindings, signalGetters, memoNames)
362
+ collectDomBindings(ir.root, domBindings, signalGetters, memoNames, undefined, new Set(), exprReadsProp)
292
363
 
293
364
  // Build consumer lists for signals
294
365
  const signalConsumers = new Map<string, string[]>()
@@ -381,8 +452,8 @@ export function buildGraphFromIR(ir: ComponentIR): ComponentGraph {
381
452
  * Callers that need the raw IR tree (events, loops, why-update) use this
382
453
  * instead of `buildComponentGraph` to avoid a redundant analysis round.
383
454
  */
384
- export function buildComponentAnalysis(source: string, filePath: string, componentName?: string): ComponentAnalysis {
385
- const ctx = analyzeComponent(source, filePath, componentName)
455
+ export function buildComponentAnalysis(source: string, filePath: string, componentName?: string, program?: ts.Program): ComponentAnalysis {
456
+ const ctx = analyzeComponent(source, filePath, componentName, program)
386
457
  const emptyIR: ComponentIR = {
387
458
  version: '0.1',
388
459
  metadata: buildMetadata(ctx),
@@ -411,8 +482,8 @@ export function buildComponentAnalysis(source: string, filePath: string, compone
411
482
  * Build a complete event summary for a component, including setter resolution
412
483
  * and downstream update paths.
413
484
  */
414
- export function buildEventSummary(source: string, filePath: string, componentName?: string): EventSummary {
415
- const { graph, ir } = buildComponentAnalysis(source, filePath, componentName)
485
+ export function buildEventSummary(source: string, filePath: string, componentName?: string, program?: ts.Program): EventSummary {
486
+ const { graph, ir } = buildComponentAnalysis(source, filePath, componentName, program)
416
487
  const setterToSignal = new Map<string, string>()
417
488
  for (const s of ir.metadata.signals) {
418
489
  if (s.setter) setterToSignal.set(s.setter, s.getter)
@@ -1473,8 +1544,8 @@ export function formatFallbackExplanations(
1473
1544
  // Component Summary (hydration/size overview)
1474
1545
  // =============================================================================
1475
1546
 
1476
- export function buildComponentSummary(source: string, filePath: string, componentName?: string): ComponentSummary {
1477
- const { graph, ir } = buildComponentAnalysis(source, filePath, componentName)
1547
+ export function buildComponentSummary(source: string, filePath: string, componentName?: string, program?: ts.Program): ComponentSummary {
1548
+ const { graph, ir } = buildComponentAnalysis(source, filePath, componentName, program)
1478
1549
  const meta = ir.metadata
1479
1550
  const clientNeeds = analyzeClientNeeds(ir)
1480
1551
  const hasReactiveState = meta.signals.length > 0 || meta.memos.length > 0 || meta.effects.length > 0
@@ -1604,7 +1675,30 @@ function collectDomBindings(
1604
1675
  signalGetters: Set<string>,
1605
1676
  memoNames: Set<string>,
1606
1677
  parentTag?: string,
1678
+ // Loop-param names in scope (#1690, #1795 Phase 2). Inside a `map(it => …)`
1679
+ // body the emitter rewrites every `it.x` read into a reactive accessor and
1680
+ // wraps the binding in `createEffect`, yet `it` is neither a signal nor a
1681
+ // memo — so without this context loop-child text / attribute bindings are
1682
+ // invisible to the graph. When a binding expression references one of these
1683
+ // names it is treated as reactive (matching the emitter's gate), giving the
1684
+ // profiler a `domBinding` (slotId + loc) to resolve `<Comp>#binding:<slotId>`.
1685
+ loopParams: Set<string> = new Set(),
1686
+ // Predicate: does an attribute expression read a component prop? Mirrors the
1687
+ // emitter's `needsEffectWrapper` prop detection so a prop-driven attribute
1688
+ // (wrapped in `createEffect` at codegen, hence emitting `#binding:<slot>`) is
1689
+ // tracked here too — otherwise its profiler id resolves to `(unresolved)`.
1690
+ readsProp: (expr: string, freeIds?: ReadonlySet<string>) => boolean = () => false,
1607
1691
  ): void {
1692
+ // Does a loop-child binding read a loop param (or index)? Use the analyzer's
1693
+ // lexer-resolved metadata, NOT a raw-string regex — so a param name that only
1694
+ // appears inside a string literal (index `i` vs `'i'`) is not mistaken for a
1695
+ // reactive read. Text expressions carry `origin.freeRefs` (a `render-item`
1696
+ // kind == map-callback param); attributes carry `freeIdentifiers` (bare
1697
+ // identifier set). This matches the emitter's actual loop-param gate.
1698
+ const exprReadsLoopParam = (n: IRExpression): boolean =>
1699
+ loopParams.size > 0 && (n.origin?.freeRefs?.some(r => loopParams.has(r.name)) ?? false)
1700
+ const attrReadsLoopParam = (free: ReadonlySet<string> | undefined): boolean =>
1701
+ loopParams.size > 0 && free !== undefined && [...loopParams].some(p => free.has(p))
1608
1702
  switch (node.type) {
1609
1703
  case 'element': {
1610
1704
  // Dynamic attribute bindings (style, class, aria-*, data-*, etc.)
@@ -1615,11 +1709,19 @@ function collectDomBindings(
1615
1709
  // deps list is the statically-proven-reactive case.
1616
1710
  for (const attr of node.attrs) {
1617
1711
  if (attr.value.kind !== 'expression' && attr.value.kind !== 'template' && attr.value.kind !== 'spread') continue
1712
+ // `key` is consumed by the loop's keyFn, never emitted as an attribute
1713
+ // effect — skip it inside loops so a `key={it.id}` read isn't mistaken
1714
+ // for a reactive binding (matches `collectLoopChildBindings`).
1715
+ if (attr.name === 'key' && loopParams.size > 0) continue
1618
1716
  const expr = attrValueToString(attr.value)
1619
1717
  if (!expr) continue
1620
1718
  const deps = extractReactiveDeps(expr, signalGetters, memoNames)
1621
- const isReactive = deps.length > 0
1622
- const wrapReason = inferWrapReasonForAttrLike(isReactive, false, attr)
1719
+ const isReactive = deps.length > 0 || attrReadsLoopParam(attr.freeIdentifiers)
1720
+ // A prop-driven attribute (`id={props.id}`, `class={`…${props.x}`}`) is
1721
+ // wrapped in a `createEffect` by the emitter even with no signal/memo
1722
+ // dep — match that gate so its `#binding:<slot>` id resolves.
1723
+ const hasPropsRef = readsProp(expr, attr.freeIdentifiers)
1724
+ const wrapReason = inferWrapReasonForAttrLike(isReactive, hasPropsRef, attr)
1623
1725
  if (wrapReason) {
1624
1726
  bindings.push({
1625
1727
  kind: 'dom',
@@ -1627,7 +1729,7 @@ function collectDomBindings(
1627
1729
  slotId: node.slotId ?? '?',
1628
1730
  deps,
1629
1731
  type: 'attribute',
1630
- classification: isReactive ? 'reactive' : 'fallback',
1732
+ classification: isReactive || hasPropsRef ? 'reactive' : 'fallback',
1631
1733
  expression: expr,
1632
1734
  wrapReason,
1633
1735
  loc: attr.loc,
@@ -1653,7 +1755,7 @@ function collectDomBindings(
1653
1755
  }
1654
1756
  // Recurse — pass element tag as parent context for text bindings
1655
1757
  for (const child of node.children) {
1656
- collectDomBindings(child, bindings, signalGetters, memoNames, node.tag)
1758
+ collectDomBindings(child, bindings, signalGetters, memoNames, node.tag, loopParams, readsProp)
1657
1759
  }
1658
1760
  break
1659
1761
  }
@@ -1661,7 +1763,8 @@ function collectDomBindings(
1661
1763
  // Widened to match emitter gate in collect-elements.ts:
1662
1764
  // `node.reactive || node.callsReactiveGetters || node.hasFunctionCalls`.
1663
1765
  const decision = decideWrapFromAstFlags(node)
1664
- if (decision.wrap && node.slotId) {
1766
+ const loopReactive = exprReadsLoopParam(node)
1767
+ if ((decision.wrap || loopReactive) && node.slotId) {
1665
1768
  const deps = extractReactiveDeps(node.expr, signalGetters, memoNames)
1666
1769
  const preview = parentTag
1667
1770
  ? `<${parentTag}>{${truncateExpr(node.expr)}}</${parentTag}>`
@@ -1672,9 +1775,12 @@ function collectDomBindings(
1672
1775
  slotId: node.slotId,
1673
1776
  deps,
1674
1777
  type: 'text',
1675
- classification: decision.reason === 'proven-reactive' ? 'reactive' : 'fallback',
1778
+ classification:
1779
+ (decision.wrap && decision.reason === 'proven-reactive') || loopReactive
1780
+ ? 'reactive'
1781
+ : 'fallback',
1676
1782
  expression: node.expr,
1677
- wrapReason: decision.reason,
1783
+ wrapReason: decision.wrap ? decision.reason : 'string-reactive',
1678
1784
  loc: node.loc,
1679
1785
  jsxPreview: preview,
1680
1786
  })
@@ -1683,7 +1789,12 @@ function collectDomBindings(
1683
1789
  }
1684
1790
  case 'conditional': {
1685
1791
  const decision = decideWrapFromAstFlags(node)
1686
- if (decision.wrap && node.slotId) {
1792
+ // A loop-child conditional whose condition reads a loop param is reactive
1793
+ // (the emitter wraps its `insert()` in a per-item effect) even though the
1794
+ // param is neither signal nor memo. Use the resolved `origin.freeRefs`.
1795
+ const loopReactive =
1796
+ loopParams.size > 0 && (node.origin?.freeRefs?.some(r => loopParams.has(r.name)) ?? false)
1797
+ if ((decision.wrap || loopReactive) && node.slotId) {
1687
1798
  const deps = extractReactiveDeps(node.condition, signalGetters, memoNames)
1688
1799
  bindings.push({
1689
1800
  kind: 'dom',
@@ -1691,15 +1802,18 @@ function collectDomBindings(
1691
1802
  slotId: node.slotId,
1692
1803
  deps,
1693
1804
  type: 'conditional',
1694
- classification: decision.reason === 'proven-reactive' ? 'reactive' : 'fallback',
1805
+ classification:
1806
+ (decision.wrap && decision.reason === 'proven-reactive') || loopReactive
1807
+ ? 'reactive'
1808
+ : 'fallback',
1695
1809
  expression: node.condition,
1696
- wrapReason: decision.reason,
1810
+ wrapReason: decision.wrap ? decision.reason : 'string-reactive',
1697
1811
  loc: node.loc,
1698
1812
  jsxPreview: `{${truncateExpr(node.condition)} ? ... : ...}`,
1699
1813
  })
1700
1814
  }
1701
- collectDomBindings(node.whenTrue, bindings, signalGetters, memoNames, parentTag)
1702
- collectDomBindings(node.whenFalse, bindings, signalGetters, memoNames, parentTag)
1815
+ collectDomBindings(node.whenTrue, bindings, signalGetters, memoNames, parentTag, loopParams, readsProp)
1816
+ collectDomBindings(node.whenFalse, bindings, signalGetters, memoNames, parentTag, loopParams, readsProp)
1703
1817
  break
1704
1818
  }
1705
1819
  case 'loop': {
@@ -1710,7 +1824,13 @@ function collectDomBindings(
1710
1824
  // (e.g. `getItems().map(...)` with an opaque helper).
1711
1825
  if (node.slotId) {
1712
1826
  const deps = extractReactiveDeps(node.array, signalGetters, memoNames)
1713
- const isReactive = deps.length > 0 || node.callsReactiveGetters === true
1827
+ // An inner loop whose array reads an outer loop param (`r.tags.map(...)`)
1828
+ // is reactive per item — use the resolved `arrayFreeIdentifiers`.
1829
+ const loopReactive =
1830
+ loopParams.size > 0 &&
1831
+ node.arrayFreeIdentifiers !== undefined &&
1832
+ [...loopParams].some(p => node.arrayFreeIdentifiers!.has(p))
1833
+ const isReactive = deps.length > 0 || node.callsReactiveGetters === true || loopReactive
1714
1834
  const isFallback = !isReactive && node.hasFunctionCalls === true
1715
1835
  if (isReactive || isFallback) {
1716
1836
  // IRLoop has no `.reactive` flag (unlike IRExpression/IRConditional),
@@ -1719,7 +1839,7 @@ function collectDomBindings(
1719
1839
  // getter (`items()` where `items` is a signal) is proven-reactive,
1720
1840
  // not fallback — flip the string evidence before handing the AST
1721
1841
  // flags to the helper.
1722
- const wrapReason: WrapReason = deps.length > 0
1842
+ const wrapReason: WrapReason = deps.length > 0 || loopReactive
1723
1843
  ? 'string-reactive'
1724
1844
  : node.callsReactiveGetters
1725
1845
  ? 'proven-reactive'
@@ -1738,8 +1858,12 @@ function collectDomBindings(
1738
1858
  })
1739
1859
  }
1740
1860
  }
1861
+ // Loop-param names enter scope for the children (#1690, #1795 Phase 2).
1862
+ const childLoopParams = new Set(loopParams)
1863
+ for (const p of extractLoopParamNames(node.param, node)) childLoopParams.add(p)
1864
+ if (node.index) childLoopParams.add(node.index)
1741
1865
  for (const child of node.children) {
1742
- collectDomBindings(child, bindings, signalGetters, memoNames, parentTag)
1866
+ collectDomBindings(child, bindings, signalGetters, memoNames, parentTag, childLoopParams, readsProp)
1743
1867
  }
1744
1868
  break
1745
1869
  }
@@ -1751,7 +1875,13 @@ function collectDomBindings(
1751
1875
  const propValue = attrValueToString(prop.value) ?? ''
1752
1876
  if (!propValue) continue
1753
1877
  const deps = extractReactiveDeps(propValue, signalGetters, memoNames)
1754
- const hasPropsRef = propValue.includes('props.')
1878
+ // Mirror the element-attr gate: a child prop is wrapped (and emits
1879
+ // `#binding:<slot>`) when it reads a prop directly or via a prop-derived
1880
+ // local const (`<Slot className={classes}>`). The previous
1881
+ // `includes('props.')` check missed both destructured props and the
1882
+ // local-const indirection, leaving the forwarded binding `(unresolved)`
1883
+ // (#1863).
1884
+ const hasPropsRef = readsProp(propValue, prop.freeIdentifiers)
1755
1885
  const isReactive = deps.length > 0 || hasPropsRef
1756
1886
  const wrapReason = inferWrapReasonForAttrLike(deps.length > 0, hasPropsRef, prop)
1757
1887
  if (wrapReason) {
@@ -1772,21 +1902,21 @@ function collectDomBindings(
1772
1902
  }
1773
1903
  }
1774
1904
  for (const child of node.children) {
1775
- collectDomBindings(child, bindings, signalGetters, memoNames, parentTag)
1905
+ collectDomBindings(child, bindings, signalGetters, memoNames, parentTag, loopParams, readsProp)
1776
1906
  }
1777
1907
  break
1778
1908
  }
1779
1909
  case 'fragment':
1780
1910
  case 'provider': {
1781
1911
  for (const child of node.children) {
1782
- collectDomBindings(child, bindings, signalGetters, memoNames, parentTag)
1912
+ collectDomBindings(child, bindings, signalGetters, memoNames, parentTag, loopParams, readsProp)
1783
1913
  }
1784
1914
  break
1785
1915
  }
1786
1916
  case 'if-statement': {
1787
- collectDomBindings(node.consequent, bindings, signalGetters, memoNames, parentTag)
1917
+ collectDomBindings(node.consequent, bindings, signalGetters, memoNames, parentTag, loopParams, readsProp)
1788
1918
  if (node.alternate) {
1789
- collectDomBindings(node.alternate, bindings, signalGetters, memoNames, parentTag)
1919
+ collectDomBindings(node.alternate, bindings, signalGetters, memoNames, parentTag, loopParams, readsProp)
1790
1920
  }
1791
1921
  break
1792
1922
  }
@@ -1798,6 +1928,40 @@ function truncateExpr(expr: string, max: number = 40): string {
1798
1928
  return s.length > max ? s.slice(0, max - 1) + '…' : s
1799
1929
  }
1800
1930
 
1931
+ /**
1932
+ * True when `expr` contains a genuine `<propsObject>.<member>` property access
1933
+ * with `member !== 'children'` — the emitter's prop gate for the props object
1934
+ * (`needsEffectWrapper`, `reactivity.ts`). Parses the expression rather than
1935
+ * regex-matching the raw string, so a `props.` inside a string literal/comment
1936
+ * doesn't false-match, and bare `props` / `props.children` are excluded. Parse
1937
+ * failures (e.g. an attribute expression carrying JSX) fall back to `false` —
1938
+ * conservative: we never invent a binding the emitter wouldn't wrap.
1939
+ */
1940
+ function exprReadsPropMember(expr: string, propsObjectName: string): boolean {
1941
+ let sf: ts.SourceFile
1942
+ try {
1943
+ sf = ts.createSourceFile('__attr.tsx', `(${expr})`, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)
1944
+ } catch {
1945
+ return false
1946
+ }
1947
+ let found = false
1948
+ const visit = (n: ts.Node): void => {
1949
+ if (found) return
1950
+ if (
1951
+ ts.isPropertyAccessExpression(n) &&
1952
+ ts.isIdentifier(n.expression) &&
1953
+ n.expression.text === propsObjectName &&
1954
+ n.name.text !== 'children'
1955
+ ) {
1956
+ found = true
1957
+ return
1958
+ }
1959
+ ts.forEachChild(n, visit)
1960
+ }
1961
+ visit(sf)
1962
+ return found
1963
+ }
1964
+
1801
1965
  /** Convert an `AttrValue` to a flat string for reactive dep extraction. */
1802
1966
  function attrValueToString(value: AttrValue): string | null {
1803
1967
  switch (value.kind) {
@@ -476,6 +476,59 @@ export function extractArrowBodyExpression(source: string): string | null {
476
476
  return expr.body.getText(sf).trim()
477
477
  }
478
478
 
479
+ /**
480
+ * A single entry of a JSX `style={{ … }}` object, lowered for SSR. The key is
481
+ * already CSS-cased (`backgroundColor` → `background-color`); the value is
482
+ * either a static string literal or a raw JS expression for the adapter to
483
+ * lower (`color` → its template interpolation).
484
+ */
485
+ export type StyleObjectEntry =
486
+ | { cssKey: string; kind: 'literal'; value: string }
487
+ | { cssKey: string; kind: 'expr'; expr: string }
488
+
489
+ /** camelCase → kebab-case for CSS property names (`backgroundColor` →
490
+ * `background-color`, `WebkitTransform` → `-webkit-transform`). The `ms`
491
+ * vendor prefix is lowercase in React style keys (`msTransform`) yet the CSS
492
+ * property carries a leading dash (`-ms-transform`), so special-case it the
493
+ * same way React's `hyphenateStyleName` does. */
494
+ export function cssKebabCase(name: string): string {
495
+ return name.replace(/[A-Z]/g, m => '-' + m.toLowerCase()).replace(/^ms-/, '-ms-')
496
+ }
497
+
498
+ /**
499
+ * Parse a JSX `style={{ … }}` object-literal source into CSS entries, or
500
+ * `null` when the shape isn't a plain object of static-keyed properties
501
+ * (spread, computed key, shorthand, method, getter) — the adapter then keeps
502
+ * refusing it. A bare `{…}` parses as a block statement, so the source is
503
+ * wrapped in parens to force expression context. Used by the template adapters
504
+ * to lower `style={{ backgroundColor: color, padding: '8px' }}` to a CSS
505
+ * string instead of emitting BF101.
506
+ */
507
+ export function parseStyleObjectEntries(source: string): StyleObjectEntry[] | null {
508
+ const sf = ts.createSourceFile('__style__.ts', `(${source})`, ts.ScriptTarget.Latest, true)
509
+ const stmt = sf.statements[0]
510
+ if (!stmt || !ts.isExpressionStatement(stmt) || sf.statements.length !== 1) return null
511
+ let expr: ts.Expression = stmt.expression
512
+ while (ts.isParenthesizedExpression(expr)) expr = expr.expression
513
+ if (!ts.isObjectLiteralExpression(expr)) return null
514
+ const entries: StyleObjectEntry[] = []
515
+ for (const prop of expr.properties) {
516
+ if (!ts.isPropertyAssignment(prop)) return null // shorthand/spread/method/getter
517
+ let key: string
518
+ if (ts.isIdentifier(prop.name)) key = prop.name.text
519
+ else if (ts.isStringLiteral(prop.name)) key = prop.name.text
520
+ else return null // computed / numeric key
521
+ const cssKey = cssKebabCase(key)
522
+ const init = prop.initializer
523
+ if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
524
+ entries.push({ cssKey, kind: 'literal', value: init.text })
525
+ } else {
526
+ entries.push({ cssKey, kind: 'expr', expr: init.getText(sf).trim() })
527
+ }
528
+ }
529
+ return entries.length > 0 ? entries : null
530
+ }
531
+
479
532
  /**
480
533
  * Parse a JavaScript expression string into a ParsedExpr tree.
481
534
  */
package/src/index.ts CHANGED
@@ -59,6 +59,9 @@ export { generateModuleExports, extractFunctionParams, formatParamWithType, find
59
59
 
60
60
  // Adapters
61
61
  export { BaseAdapter } from './adapters/interface.ts'
62
+ // Dependency-free adapter for tooling that only needs client JS (e.g. the
63
+ // profiler scenario driver) — the client output is adapter-independent.
64
+ export { TestAdapter, testAdapter } from './adapters/test-adapter.ts'
62
65
  export type {
63
66
  TemplateAdapter,
64
67
  AdapterOutput,
@@ -244,10 +247,12 @@ export {
244
247
  export { ErrorCodes, createError, formatError, generateCodeFrame } from './errors.ts'
245
248
 
246
249
  // Expression Parser
247
- export { parseExpression, isSupported, exprToString, stringifyParsedExpr, identifierPath, parseBlockBody, containsHigherOrder, extractArrowBodyExpression } from './expression-parser.ts'
250
+ export { parseExpression, isSupported, exprToString, stringifyParsedExpr, identifierPath, parseBlockBody, containsHigherOrder, extractArrowBodyExpression, parseStyleObjectEntries } from './expression-parser.ts'
251
+ export type { StyleObjectEntry } from './expression-parser.ts'
248
252
  export type { ParsedExpr, ParsedStatement, SortComparator, SortKey, ReduceOp, FlatDepth, FlatMapOp, FlatMapLeaf, SupportLevel, SupportResult, TemplatePart } from './expression-parser.ts'
249
253
  export { buildLoopChainExpr } from './loop-chain.ts'
250
254
  export type { LoopChainInputs } from './loop-chain.ts'
255
+ export { isLowerableObjectRestDestructure } from './loop-destructure.ts'
251
256
 
252
257
  // Debug analysis
253
258
  export {
@@ -277,6 +282,72 @@ export {
277
282
  export type { ComponentGraph, ComponentAnalysis, SignalNode, MemoNode, EffectNode, DomBinding, UpdatePath, SignalTrace, EventBinding, SetterRef, FnSetterResolution, EventSummary, LoopInfo, LoopChildBinding, LoopSummary, WhyUpdateResult, WhyUpdateDep, WhyUpdateSource, FallbackExplanation, ComponentSummary } from './debug.ts'
278
283
  export type { WrapReason } from './ir-to-client-js/reactivity.ts'
279
284
 
285
+ // Reactive performance profiler (#1690). Static half (SR5 budget, SR6 diff) +
286
+ // dynamic half (SR2/SR4 join, SR7 report, v1 analyses).
287
+ export {
288
+ buildStaticBudget,
289
+ formatStaticBudget,
290
+ diffStaticBudget,
291
+ formatBudgetDiff,
292
+ buildProfileReport,
293
+ formatProfileReport,
294
+ buildIdIndex,
295
+ joinProfilerEvents,
296
+ parseProfilerId,
297
+ analyzeHotSubscribers,
298
+ formatHotSubscribers,
299
+ findUninstrumentedEffects,
300
+ analyzeWastedReReruns,
301
+ formatWastedReReruns,
302
+ analyzeBatchAdvisor,
303
+ formatBatchAdvisor,
304
+ } from './profiler.ts'
305
+ export type {
306
+ StaticBudget,
307
+ StaticBudgetOptions,
308
+ FanOutEntry,
309
+ BudgetDiff,
310
+ FanOutChange,
311
+ ProfileReport,
312
+ ProfileReportInput,
313
+ ProfileCoverage,
314
+ DiagnosticsSummary,
315
+ EffectCandidate,
316
+ IdIndex,
317
+ ResolvedNode,
318
+ JoinResult,
319
+ JoinedEvent,
320
+ UnattributedId,
321
+ HotSubscribersResult,
322
+ HotSubscriber,
323
+ HotSubscribersOptions,
324
+ WastedReRunsResult,
325
+ WastedSubscriber,
326
+ WastedReRunsOptions,
327
+ BatchAdvisorResult,
328
+ BatchCandidate,
329
+ BatchSafety,
330
+ } from './profiler.ts'
331
+
332
+ // Reactive profile — findings layer (#1690 dogfood: Bug A/C/D fixes, batch-candidate dedup,
333
+ // fallback-heavy detection, multi-component table, SR6 compile-diff).
334
+ export {
335
+ buildReactiveProfile,
336
+ buildProfileFromGraph,
337
+ diffProfiles,
338
+ formatSingleProfile,
339
+ formatProfileTable,
340
+ formatProfileDiff,
341
+ profileToJSON,
342
+ } from './debug-profile.ts'
343
+ export type {
344
+ ComponentProfile,
345
+ ComponentProfileMetrics,
346
+ ProfileFinding,
347
+ ProfileDiff,
348
+ ProfileDiffEntry,
349
+ } from './debug-profile.ts'
350
+
280
351
  // HTML constants
281
352
  export { BOOLEAN_ATTRS, isBooleanAttr } from './html-constants.ts'
282
353
 
@@ -70,6 +70,12 @@ export interface BranchPlainLoopPlan {
70
70
  * multi-line renderItem with multi-root template clone (#1212).
71
71
  */
72
72
  bodyIsMultiRoot: boolean
73
+ /**
74
+ * Profile-mode loop id (#1690, #1795 Phase 3): `<Component>#binding:<slotId>`
75
+ * for the branch loop's `mapArray` (the loop node shares its container slot).
76
+ * Undefined off → byte-identical (SR8).
77
+ */
78
+ profileLoopId?: string
73
79
  }
74
80
 
75
81
  export interface BranchCompositeLoopPlan {
@@ -22,7 +22,7 @@ import type {
22
22
  BranchPlainLoopPlan,
23
23
  } from './branch-loop.ts'
24
24
 
25
- export function buildBranchLoopPlan(loop: BranchLoop): BranchLoopPlan {
25
+ export function buildBranchLoopPlan(loop: BranchLoop, profileComponentName?: string): BranchLoopPlan {
26
26
  const containerSlotId = loop.containerSlotId
27
27
  const cv = varSlotId(containerSlotId)
28
28
  const containerVar = `__loop_${cv}`
@@ -30,7 +30,7 @@ export function buildBranchLoopPlan(loop: BranchLoop): BranchLoopPlan {
30
30
  if (loop.useElementReconciliation && (loop.nestedComponents?.length || loop.innerLoops?.length)) {
31
31
  const composite: BranchCompositeLoopPlan = {
32
32
  kind: 'composite',
33
- composite: buildBranchCompositePlan(loop, cv),
33
+ composite: buildBranchCompositePlan(loop, cv, profileComponentName),
34
34
  containerSlotId,
35
35
  containerVar,
36
36
  }
@@ -65,11 +65,13 @@ export function buildBranchLoopPlan(loop: BranchLoop): BranchLoopPlan {
65
65
  conditionals: loop.bindings.conditionals,
66
66
  loopParam: loop.param,
67
67
  loopParamBindings: loop.paramBindings,
68
+ profileComponentName,
68
69
  })
69
70
  : null,
70
- eventDelegation: buildBranchLoopDelegationPlan(loop, cv),
71
+ eventDelegation: buildBranchLoopDelegationPlan(loop, cv, profileComponentName),
71
72
  childRefs: buildChildRefBindings(loop.bindings.refs, loop.param, loop.paramBindings),
72
73
  bodyIsMultiRoot: loop.bodyIsMultiRoot ?? false,
74
+ profileLoopId: profileComponentName ? `${profileComponentName}#binding:${containerSlotId}` : undefined,
73
75
  }
74
76
  return plan
75
77
  }
@@ -31,7 +31,7 @@ import { buildReactiveEffectsPlan } from './build-reactive-effects.ts'
31
31
  import type { ComponentLoopPlan, NestedComponentInit } from './types.ts'
32
32
 
33
33
  /** @internal — prefer `buildLoopPlan`. */
34
- export function buildComponentLoopPlan(elem: TopLevelLoop): ComponentLoopPlan {
34
+ export function buildComponentLoopPlan(elem: TopLevelLoop, profileComponentName?: string): ComponentLoopPlan {
35
35
  const { name } = elem.childComponent!
36
36
  const propsExpr = buildComponentPropsExpr(elem.childComponent!, elem.param)
37
37
  const keyExpr = wrapLoopParamAsAccessor(elem.key || '__idx', elem.param, elem.paramBindings)
@@ -77,6 +77,7 @@ export function buildComponentLoopPlan(elem: TopLevelLoop): ComponentLoopPlan {
77
77
  // by the type so the structural invariant (every variant has a
78
78
  // `childRefs`) is preserved; populated as empty.
79
79
  childRefs: buildChildRefBindings(elem.bindings.refs, elem.param, elem.paramBindings),
80
+ profileLoopId: profileComponentName ? `${profileComponentName}#binding:${elem.slotId}` : undefined,
80
81
  childConditionalEffects: hasChildConds
81
82
  ? buildReactiveEffectsPlan({
82
83
  attrs: [],
@@ -84,6 +85,7 @@ export function buildComponentLoopPlan(elem: TopLevelLoop): ComponentLoopPlan {
84
85
  conditionals: elem.bindings.conditionals,
85
86
  loopParam: elem.param,
86
87
  loopParamBindings: elem.paramBindings,
88
+ profileComponentName,
87
89
  })
88
90
  : null,
89
91
  }
@@ -26,7 +26,7 @@ import { buildInnerLoopsPlan } from './build-inner-loop.ts'
26
26
  import type { CompositeLoopPlan } from './types.ts'
27
27
 
28
28
  /** @internal — prefer `buildLoopPlan`. */
29
- export function buildTopLevelCompositePlan(elem: TopLevelLoop): CompositeLoopPlan {
29
+ export function buildTopLevelCompositePlan(elem: TopLevelLoop, profileComponentName?: string): CompositeLoopPlan {
30
30
  const nestedComps = elem.nestedComponents!
31
31
  const depthLevels = buildDepthLevels(elem.innerLoops ?? [], nestedComps, elem.bindings.events)
32
32
  const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(elem.param, elem.paramBindings)
@@ -63,16 +63,19 @@ export function buildTopLevelCompositePlan(elem: TopLevelLoop): CompositeLoopPla
63
63
  conditionals: elem.bindings.conditionals,
64
64
  loopParam: elem.param,
65
65
  loopParamBindings: elem.paramBindings,
66
+ profileComponentName,
66
67
  })
67
68
  : null,
68
69
  branchClearChildren: false,
69
70
  topIndent: ' ',
70
71
  bodyIndent: ' ',
71
72
  bodyIsMultiRoot: elem.bodyIsMultiRoot ?? false,
73
+ profileLoopId: profileComponentName ? `${profileComponentName}#binding:${elem.slotId}` : undefined,
74
+ profileComponentName,
72
75
  }
73
76
  }
74
77
 
75
- export function buildBranchCompositePlan(loop: BranchLoop, cv: string): CompositeLoopPlan {
78
+ export function buildBranchCompositePlan(loop: BranchLoop, cv: string, profileComponentName?: string): CompositeLoopPlan {
76
79
  const nestedComps = loop.nestedComponents!
77
80
  const innerLoops = loop.innerLoops ?? []
78
81
  const childEvents = loop.bindings.events
@@ -114,12 +117,15 @@ export function buildBranchCompositePlan(loop: BranchLoop, cv: string): Composit
114
117
  conditionals: loop.bindings.conditionals,
115
118
  loopParam: loop.param,
116
119
  loopParamBindings: loop.paramBindings,
120
+ profileComponentName,
117
121
  })
118
122
  : null,
119
123
  branchClearChildren: true,
120
124
  topIndent: ' ',
121
125
  bodyIndent: ' ',
122
126
  bodyIsMultiRoot: loop.bodyIsMultiRoot ?? false,
127
+ profileLoopId: profileComponentName ? `${profileComponentName}#binding:${loop.containerSlotId}` : undefined,
128
+ profileComponentName,
123
129
  }
124
130
  }
125
131