@barefootjs/jsx 0.10.1 → 0.12.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 +1881 -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 +502 -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 +466 -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 +1520 -0
  137. package/src/types.ts +8 -0
@@ -62,6 +62,7 @@ export function buildDeclarationEmitPlan(
62
62
  kind: 'memo',
63
63
  name: decl.info.name,
64
64
  computationExpr: decl.info.computation,
65
+ bfId: ctx.profile ? `${ctx.componentName}#memo:${decl.info.name}` : undefined,
65
66
  }
66
67
  case 'function': {
67
68
  const fn = decl.info
@@ -84,13 +85,18 @@ function buildSignalPlan(
84
85
  ctx: ClientJsContext,
85
86
  lookups: DeclarationEmitLookups,
86
87
  ): SignalEmitPlan {
88
+ const controlledEffect = buildControlledSignalEffect(signal, lookups)
89
+ if (controlledEffect && ctx.profile) {
90
+ controlledEffect.bfId = `${ctx.componentName}#effect:controlled:${signal.setter}`
91
+ }
87
92
  return {
88
93
  kind: 'signal',
89
94
  getter: signal.getter,
90
95
  setter: signal.setter,
91
96
  initialValueExpr: resolveSignalInitialValue(signal, ctx, lookups),
92
- controlledEffect: buildControlledSignalEffect(signal, lookups),
97
+ controlledEffect,
93
98
  branchCondition: signal.branchCondition,
99
+ bfId: ctx.profile ? `${ctx.componentName}#signal:${signal.getter}` : undefined,
94
100
  }
95
101
  }
96
102
 
@@ -52,6 +52,8 @@ export interface SignalEmitPlan {
52
52
  * cell #8.
53
53
  */
54
54
  branchCondition?: string
55
+ /** Profile-mode IR-aligned id, appended as the `createSignal` 2nd arg (#1690). */
56
+ bfId?: string
55
57
  }
56
58
 
57
59
  export interface ControlledSignalEffectPlan {
@@ -59,6 +61,8 @@ export interface ControlledSignalEffectPlan {
59
61
  setter: string
60
62
  /** Fully-resolved accessor expression read inside the effect. */
61
63
  accessorExpr: string
64
+ /** Profile-mode IR-aligned id, appended as the `createEffect` 2nd arg (#1690). */
65
+ bfId?: string
62
66
  }
63
67
 
64
68
  export interface MemoEmitPlan {
@@ -66,6 +70,8 @@ export interface MemoEmitPlan {
66
70
  name: string
67
71
  /** Source expression passed to `createMemo(...)`. */
68
72
  computationExpr: string
73
+ /** Profile-mode IR-aligned id, appended as the `createMemo` 2nd arg (#1690). */
74
+ bfId?: string
69
75
  }
70
76
 
71
77
  export interface FunctionEmitPlan {
@@ -59,7 +59,13 @@ function emitConstant(lines: string[], plan: ConstantEmitPlan): void {
59
59
  }
60
60
  }
61
61
 
62
+ /** Profile-mode trailing `__bfId` argument (`, "Comp#signal:x"`), or '' when off. */
63
+ function bfIdArg(bfId: string | undefined): string {
64
+ return bfId ? `, ${JSON.stringify(bfId)}` : ''
65
+ }
66
+
62
67
  function emitSignal(lines: string[], plan: SignalEmitPlan): void {
68
+ const id = bfIdArg(plan.bfId)
63
69
  if (plan.branchCondition) {
64
70
  // #1414 cell #8: signal declared inside an early-return `if`-block.
65
71
  // Hoist as `let` so closures and event handlers hoisted to outer
@@ -70,12 +76,12 @@ function emitSignal(lines: string[], plan: SignalEmitPlan): void {
70
76
  if (plan.setter) {
71
77
  lines.push(` let ${plan.getter}, ${plan.setter}`)
72
78
  lines.push(` if (${plan.branchCondition}) {`)
73
- lines.push(` ;[${plan.getter}, ${plan.setter}] = createSignal(${plan.initialValueExpr})`)
79
+ lines.push(` ;[${plan.getter}, ${plan.setter}] = createSignal(${plan.initialValueExpr}${id})`)
74
80
  lines.push(` }`)
75
81
  } else {
76
82
  lines.push(` let ${plan.getter}`)
77
83
  lines.push(` if (${plan.branchCondition}) {`)
78
- lines.push(` ;[${plan.getter}] = createSignal(${plan.initialValueExpr})`)
84
+ lines.push(` ;[${plan.getter}] = createSignal(${plan.initialValueExpr}${id})`)
79
85
  lines.push(` }`)
80
86
  }
81
87
  // Controlled effects don't apply to branch-conditioned signals — a
@@ -84,9 +90,9 @@ function emitSignal(lines: string[], plan: SignalEmitPlan): void {
84
90
  return
85
91
  }
86
92
  if (plan.setter) {
87
- lines.push(` const [${plan.getter}, ${plan.setter}] = createSignal(${plan.initialValueExpr})`)
93
+ lines.push(` const [${plan.getter}, ${plan.setter}] = createSignal(${plan.initialValueExpr}${id})`)
88
94
  } else {
89
- lines.push(` const [${plan.getter}] = createSignal(${plan.initialValueExpr})`)
95
+ lines.push(` const [${plan.getter}] = createSignal(${plan.initialValueExpr}${id})`)
90
96
  }
91
97
  if (plan.controlledEffect) {
92
98
  emitControlledEffect(lines, plan.controlledEffect)
@@ -97,11 +103,11 @@ function emitControlledEffect(lines: string[], plan: ControlledSignalEffectPlan)
97
103
  lines.push(` createEffect(() => {`)
98
104
  lines.push(` const __val = ${plan.accessorExpr}`)
99
105
  lines.push(` if (__val !== undefined) ${plan.setter}(__val)`)
100
- lines.push(` })`)
106
+ lines.push(` }${bfIdArg(plan.bfId)})`)
101
107
  }
102
108
 
103
109
  function emitMemo(lines: string[], plan: MemoEmitPlan): void {
104
- lines.push(` const ${plan.name} = createMemo(${plan.computationExpr})`)
110
+ lines.push(` const ${plan.name} = createMemo(${plan.computationExpr}${bfIdArg(plan.bfId)})`)
105
111
  }
106
112
 
107
113
  function emitFunction(lines: string[], plan: FunctionEmitPlan): void {
@@ -40,6 +40,11 @@ export interface ClientJsContext {
40
40
  * collide with same-named exports from other modules.
41
41
  */
42
42
  nonExportedSiblings: Set<string>
43
+ /**
44
+ * Profile mode (#1690, SR3). When true, emit IR-aligned `__bfId` args at
45
+ * reactive creation sites. Off by default → byte-identical output (SR8).
46
+ */
47
+ profile: boolean
43
48
  signals: SignalInfo[]
44
49
  memos: MemoInfo[]
45
50
  effects: EffectInfo[]
@@ -50,6 +50,25 @@ export function varSlotId(slotId: string): string {
50
50
  return slotId.startsWith('^') ? slotId.slice(1) : slotId
51
51
  }
52
52
 
53
+ /**
54
+ * Profile-mode DOM-binding id suffix (#1690, SR4). Returns the trailing
55
+ * `, "<Component>#binding:<slotId>"` argument for a binding effect's
56
+ * `createEffect` / `createDisposableEffect` / `insert` / `mapArray` call when
57
+ * `componentName` is set (profile on), else `''` so the emitted code stays
58
+ * byte-identical (SR8). Centralised so every binding emit site — top-level,
59
+ * conditional branch, loop child, inner loop — uses one id convention.
60
+ *
61
+ * A `'?'` slotId (an inner loop whose container element carries no `bf` slot
62
+ * marker) is NOT emittable: `buildIdIndex` keys on real `domBinding` slotIds, so
63
+ * `#binding:?` could never resolve and would be a guaranteed coverage gap. The
64
+ * analyzer likewise emits no `domBinding` for a slot-less loop, so suppressing
65
+ * the id here keeps both sides silent and consistent.
66
+ */
67
+ export function profileBindingId(componentName: string | undefined, slotId: string): string {
68
+ if (!componentName || slotId === '?') return ''
69
+ return `, ${JSON.stringify(`${componentName}#binding:${slotId}`)}`
70
+ }
71
+
53
72
  /**
54
73
  * Convert a `template` variant's parts into a JS template-literal string.
55
74
  * Shared by both `attrValueToString` and any consumer that wants to flatten
@@ -241,6 +260,24 @@ export function wrapHandlerInBlock(handler: string): string {
241
260
  return trimmed
242
261
  }
243
262
 
263
+ /**
264
+ * Profile mode (#1690, SR3): wrap an event handler so a profiling run can
265
+ * attribute the reactive work it triggers to one turn. The original handler
266
+ * expression (arrow or identifier) is invoked verbatim with the forwarded
267
+ * args, bracketed by `beginTurn`/`endTurn`:
268
+ *
269
+ * (...__bfa) => { beginTurn("Comp#handler:slot:click"); try { return (HANDLER)(...__bfa) } finally { endTurn() } }
270
+ *
271
+ * Measurement-only: the handler's behavior and the synchronous `set()`
272
+ * semantics are unchanged — the markers just stamp a turn id onto the events
273
+ * emitted while it runs. Used at every handler emit site in profile mode so
274
+ * no path is left unattributed.
275
+ */
276
+ export function wrapHandlerForTurn(handler: string, handlerId: string, loc?: string): string {
277
+ const idArg = loc ? `${JSON.stringify(handlerId)}, ${JSON.stringify(loc)}` : JSON.stringify(handlerId)
278
+ return `(...__bfa) => { beginTurn(${idArg}); try { return (${handler.trim()})(...__bfa) } finally { endTurn() } }`
279
+ }
280
+
244
281
  /**
245
282
  * Emit a ref-binding call `(callback)(elementVar)`, optionally guarded so the
246
283
  * call no-ops when the callback is undefined.
package/src/jsx-to-ir.ts CHANGED
@@ -33,7 +33,7 @@ import {
33
33
  AttrValueOf,
34
34
  } from './types.ts'
35
35
  import { type AnalyzerContext, type MultiReturnJsxInfo, getSourceLocation } from './analyzer-context.ts'
36
- import { parseExpression, isSupported, parseBlockBody, extractSortComparatorFromTS, type ParsedExpr, type ParsedStatement, type SortComparator } from './expression-parser.ts'
36
+ import { parseExpression, isSupported, parseBlockBody, extractSortComparatorFromTS, cssKebabCase, type ParsedExpr, type ParsedStatement, type SortComparator } from './expression-parser.ts'
37
37
  import { createError, ErrorCodes, internalInvariant } from './errors.ts'
38
38
  import { containsReactiveExpression } from './reactivity-checker.ts'
39
39
  import {
@@ -3804,7 +3804,7 @@ function tryStaticStyleObjectToCss(expr: ts.ObjectLiteralExpression): string | n
3804
3804
  if (!ts.isPropertyAssignment(prop)) return null
3805
3805
  if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name)) return null
3806
3806
  if (!ts.isStringLiteral(prop.initializer)) return null
3807
- const key = prop.name.text.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)
3807
+ const key = cssKebabCase(prop.name.text)
3808
3808
  parts.push(`${key}:${prop.initializer.text}`)
3809
3809
  }
3810
3810
  return parts.join(';')
@@ -0,0 +1,170 @@
1
+ import type { IRLoop, IRNode, AttrValue, IRTemplatePart } from './types.ts'
2
+
3
+ const SIMPLE_FIELD = /^\.[A-Za-z_$][\w$]*$/
4
+
5
+ /**
6
+ * True when a loop's destructure param is the single shape the non-JS SSR
7
+ * adapters (Go template / Mojolicious / Xslate) can lower today:
8
+ *
9
+ * `arr.map(({ id, title, ...rest }) => …)`
10
+ *
11
+ * where every binding is a simple `.field` access or an **object-rest read only
12
+ * via member access** (`rest.flag`). The adapters lower the rest binding as an
13
+ * alias to the whole iteration item, which matches JS rest semantics only for
14
+ * member reads of non-consumed keys — so any other use must be refused:
15
+ *
16
+ * - array-rest / array-index / nested paths (`[a, ...t]`, `{ cells: [h] }`)
17
+ * need index/slice the `range`/`for` can't express inline;
18
+ * - spread (`{...rest}`) and bare value uses (`String(rest)`, `{rest}`,
19
+ * `fn(rest)`) would observe the consumed keys too — they need a residual
20
+ * object the templates can't build inline;
21
+ * - a chained `.filter().map(destructure)` would need the filter-param
22
+ * rewrite to target the synthetic per-item var, so it's refused as well.
23
+ *
24
+ * Unsupported shapes fall through to the adapters' BF104 diagnostic. The scan
25
+ * is conservative: an ambiguous use refuses (false-negative is safe — it keeps
26
+ * the existing build-time error rather than shipping wrong output).
27
+ */
28
+ export function isLowerableObjectRestDestructure(loop: IRLoop): boolean {
29
+ const bindings = loop.paramBindings
30
+ if (!bindings || bindings.length === 0) return false
31
+ if (loop.filterPredicate) return false
32
+ // The SSR adapters emit a synthetic per-item loop variable in the reserved
33
+ // `__bf_item` namespace (depth-suffixed on Go). A user destructure binding —
34
+ // or the `index` param — in that namespace would collide with / shadow the
35
+ // synthetic var (duplicate `my $__bf_item …` locals, ambiguous accessors), so
36
+ // refuse the lowering (→ BF104) rather than emit broken template locals.
37
+ for (const name of [...bindings.map(b => b.name), loop.index]) {
38
+ if (name && name.startsWith('__bf_')) return false
39
+ }
40
+ for (const b of bindings) {
41
+ if (b.rest) {
42
+ if (b.rest.kind !== 'object') return false
43
+ } else if (!SIMPLE_FIELD.test(b.path)) {
44
+ return false
45
+ }
46
+ }
47
+ const restNames = bindings.filter(b => b.rest).map(b => b.name)
48
+ if (restNames.length === 0) return true
49
+ return !restNamesMisused(loop, restNames)
50
+ }
51
+
52
+ /**
53
+ * Walks the whole loop subtree (every node type, every expression-bearing
54
+ * field) and reports whether any rest name is referenced as something other
55
+ * than a member-access base (`rest.flag` / `rest?.flag`). A spread expr
56
+ * (`{...rest}` → expr `"rest"`) and bare value uses are caught by the same
57
+ * regex; a property of an unrelated object (`foo.rest`) is excluded by the
58
+ * lookbehind.
59
+ *
60
+ * The scan covers the gated loop's own non-children expression fields too
61
+ * (`array` / `key` / `mapPreamble` / `flatMapCallback` body) — a bare rest use
62
+ * can surface there (e.g. `.map(({ ...rest }) => { const x = rest; … })`
63
+ * lifts `const x = rest` into `mapPreamble`), plus intrinsic element event
64
+ * handlers (`onClick={() => fn(rest)}`).
65
+ */
66
+ function restNamesMisused(loop: IRLoop, names: string[]): boolean {
67
+ const valueUse = names.map(
68
+ n => new RegExp(`(?<![\\w.$])${escapeRe(n)}(?!\\s*\\??\\.)(?![\\w$])`),
69
+ )
70
+ let misused = false
71
+ const check = (s: string | undefined | null): void => {
72
+ if (!s || misused) return
73
+ for (const re of valueUse) {
74
+ if (re.test(s)) {
75
+ misused = true
76
+ return
77
+ }
78
+ }
79
+ }
80
+ const attr = (v: AttrValue): void => {
81
+ if (v.kind === 'expression' || v.kind === 'spread') {
82
+ check(v.expr)
83
+ check(v.templateExpr)
84
+ } else if (v.kind === 'template') {
85
+ v.parts.forEach(part)
86
+ }
87
+ }
88
+ const part = (p: IRTemplatePart): void => {
89
+ if (p.type === 'ternary') {
90
+ check(p.condition)
91
+ check(p.templateCondition)
92
+ check(p.whenTrue)
93
+ check(p.whenFalse)
94
+ } else if (p.type === 'lookup') {
95
+ check(p.key)
96
+ check(p.templateKey)
97
+ }
98
+ }
99
+ const visitLoop = (l: IRLoop): void => {
100
+ if (misused) return
101
+ check(l.array)
102
+ check(l.templateArray)
103
+ check(l.key)
104
+ check(l.mapPreamble)
105
+ check(l.templateMapPreamble)
106
+ if (l.flatMapCallback) {
107
+ check(l.flatMapCallback.body)
108
+ check(l.flatMapCallback.templateBody)
109
+ l.flatMapCallback.fragments.forEach(f => visit(f.ir))
110
+ }
111
+ l.children.forEach(visit)
112
+ }
113
+ const visit = (node: IRNode): void => {
114
+ if (misused) return
115
+ switch (node.type) {
116
+ case 'expression':
117
+ check(node.expr)
118
+ check(node.templateExpr)
119
+ break
120
+ case 'conditional':
121
+ check(node.condition)
122
+ check(node.templateCondition)
123
+ visit(node.whenTrue)
124
+ visit(node.whenFalse)
125
+ break
126
+ case 'if-statement':
127
+ check(node.condition)
128
+ check(node.templateCondition)
129
+ for (const sv of node.scopeVariables) {
130
+ check(sv.initializer)
131
+ check(sv.templateInitializer)
132
+ }
133
+ visit(node.consequent)
134
+ if (node.alternate) visit(node.alternate)
135
+ break
136
+ case 'element':
137
+ node.attrs.forEach(a => attr(a.value))
138
+ node.events.forEach(e => check(e.handler))
139
+ node.children.forEach(visit)
140
+ break
141
+ case 'component':
142
+ node.props.forEach(p => attr(p.value))
143
+ node.children.forEach(visit)
144
+ break
145
+ case 'provider':
146
+ attr(node.valueProp.value)
147
+ node.children.forEach(visit)
148
+ break
149
+ case 'fragment':
150
+ node.children.forEach(visit)
151
+ break
152
+ case 'async':
153
+ visit(node.fallback)
154
+ node.children.forEach(visit)
155
+ break
156
+ case 'loop':
157
+ visitLoop(node)
158
+ break
159
+ case 'text':
160
+ case 'slot':
161
+ break
162
+ }
163
+ }
164
+ visitLoop(loop)
165
+ return misused
166
+ }
167
+
168
+ function escapeRe(s: string): string {
169
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
170
+ }