@barefootjs/jsx 0.29.0 → 0.30.2

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 (90) hide show
  1. package/dist/adapters/child-scope.d.ts +17 -0
  2. package/dist/adapters/child-scope.d.ts.map +1 -0
  3. package/dist/adapters/loop-bound-names.d.ts.map +1 -1
  4. package/dist/analyzer-context.d.ts.map +1 -1
  5. package/dist/analyzer.d.ts.map +1 -1
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +990 -367
  9. package/dist/ir-to-client-js/collect-elements.d.ts +5 -25
  10. package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
  11. package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts +66 -2
  12. package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts.map +1 -1
  13. package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts.map +1 -1
  14. package/dist/ir-to-client-js/control-flow/plan/lazy-conditional.d.ts +92 -0
  15. package/dist/ir-to-client-js/control-flow/plan/lazy-conditional.d.ts.map +1 -0
  16. package/dist/ir-to-client-js/control-flow/plan/lazy-preamble.d.ts +134 -0
  17. package/dist/ir-to-client-js/control-flow/plan/lazy-preamble.d.ts.map +1 -0
  18. package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.d.ts +46 -11
  19. package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.d.ts.map +1 -1
  20. package/dist/ir-to-client-js/control-flow/plan/reactive-effects.d.ts +7 -0
  21. package/dist/ir-to-client-js/control-flow/plan/reactive-effects.d.ts.map +1 -1
  22. package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts.map +1 -1
  23. package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
  24. package/dist/ir-to-client-js/html-template.d.ts +2 -3
  25. package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
  26. package/dist/ir-to-client-js/plan/declaration-emit.d.ts +6 -0
  27. package/dist/ir-to-client-js/plan/declaration-emit.d.ts.map +1 -1
  28. package/dist/ir-to-client-js/reactivity.d.ts +1 -10
  29. package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
  30. package/dist/ir-to-client-js/types.d.ts +12 -0
  31. package/dist/ir-to-client-js/types.d.ts.map +1 -1
  32. package/dist/jsx-to-ir.d.ts.map +1 -1
  33. package/dist/prop-rewrite.d.ts +24 -7
  34. package/dist/prop-rewrite.d.ts.map +1 -1
  35. package/dist/rich-type-evidence.d.ts.map +1 -1
  36. package/dist/ssr-defaults.d.ts.map +1 -1
  37. package/dist/to-locale-date-lowering.d.ts.map +1 -1
  38. package/dist/types.d.ts +78 -0
  39. package/dist/types.d.ts.map +1 -1
  40. package/package.json +2 -2
  41. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +6 -24
  42. package/src/__tests__/branch-loop-plain.test.ts +28 -45
  43. package/src/__tests__/csr-substitution-safety-divergence.test.ts +9 -3
  44. package/src/__tests__/csr-template-scope-soundness.test.ts +167 -0
  45. package/src/__tests__/early-return-branch-switch.test.ts +96 -0
  46. package/src/__tests__/form-control-value-ssr.test.ts +102 -0
  47. package/src/__tests__/lazy-conditional.test.ts +190 -0
  48. package/src/__tests__/lazy-preamble.test.ts +297 -0
  49. package/src/__tests__/lazy-row-eligibility.test.ts +97 -28
  50. package/src/__tests__/loop-item-root-scope.test.ts +213 -0
  51. package/src/__tests__/map-multi-return-body.test.ts +30 -3
  52. package/src/__tests__/nested-loop-conditional.test.ts +27 -6
  53. package/src/__tests__/preamble-attr-reactivity.test.ts +217 -0
  54. package/src/__tests__/preamble-declarations.test.ts +207 -0
  55. package/src/__tests__/ssr-defaults.test.ts +44 -0
  56. package/src/adapters/child-scope.ts +22 -0
  57. package/src/adapters/loop-bound-names.ts +6 -0
  58. package/src/analyzer-context.ts +43 -0
  59. package/src/analyzer.ts +33 -3
  60. package/src/index.ts +1 -0
  61. package/src/ir-to-client-js/collect-elements.ts +51 -10
  62. package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +1 -1
  63. package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +2 -2
  64. package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +3 -3
  65. package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +2 -2
  66. package/src/ir-to-client-js/control-flow/plan/build-lazy-row.ts +147 -5
  67. package/src/ir-to-client-js/control-flow/plan/build-loop.ts +2 -2
  68. package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +1 -0
  69. package/src/ir-to-client-js/control-flow/plan/lazy-conditional.ts +151 -0
  70. package/src/ir-to-client-js/control-flow/plan/lazy-preamble.ts +287 -0
  71. package/src/ir-to-client-js/control-flow/plan/lazy-row-eligibility.ts +85 -22
  72. package/src/ir-to-client-js/control-flow/plan/reactive-effects.ts +7 -0
  73. package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +133 -35
  74. package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +24 -4
  75. package/src/ir-to-client-js/csr-substitute.ts +6 -1
  76. package/src/ir-to-client-js/emit-registration.ts +1 -1
  77. package/src/ir-to-client-js/html-template.ts +51 -40
  78. package/src/ir-to-client-js/index.ts +1 -1
  79. package/src/ir-to-client-js/plan/build-declaration-emit.ts +2 -0
  80. package/src/ir-to-client-js/plan/build-static-array-child-init.ts +1 -1
  81. package/src/ir-to-client-js/plan/declaration-emit.ts +6 -0
  82. package/src/ir-to-client-js/reactivity.ts +25 -0
  83. package/src/ir-to-client-js/stringify/declaration-emit.ts +9 -4
  84. package/src/ir-to-client-js/types.ts +12 -0
  85. package/src/jsx-to-ir.ts +409 -24
  86. package/src/prop-rewrite.ts +149 -19
  87. package/src/rich-type-evidence.ts +6 -7
  88. package/src/ssr-defaults.ts +7 -2
  89. package/src/to-locale-date-lowering.ts +8 -2
  90. package/src/types.ts +80 -0
@@ -3,39 +3,165 @@
3
3
  *
4
4
  * Walks the TypeScript AST to identify destructured prop names used as
5
5
  * value references (not as object keys, property access targets, or
6
- * shorthand properties), then applies targeted regex replacement.
6
+ * shorthand properties), then splices `_p.` onto exactly those
7
+ * references in the emitted text via a second scope-aware walk over
8
+ * the text's own AST.
9
+ *
10
+ * Scope model: a name bound INSIDE the expression — a nested arrow /
11
+ * function parameter, a block-scoped declaration, a catch variable —
12
+ * refers to that binding, not the prop, for the whole binding's scope.
13
+ * Both the discovery walk and the rewrite walk carry the same binding
14
+ * stack, so `items.map((title) => title.a)` never turns into the
15
+ * syntactically invalid `.map((_p.title) => _p.title.a)` when `title`
16
+ * is also a prop. (Names bound by loop callbacks that ENCLOSE the
17
+ * expression are the caller's job — see the `ctx.loopParams` filter in
18
+ * `jsx-to-ir.ts`'s `rewriteBarePropRefs` wrapper, #2222.)
7
19
  */
8
20
 
9
21
  import ts from 'typescript'
10
22
  import { PROPS_PARAM } from './ir-to-client-js/utils.ts'
11
23
  import { createTemplateAwareStringProtector } from './ir-to-client-js/html-template.ts'
12
24
 
25
+ /** Collect every name introduced by a binding name (identifier or pattern). */
26
+ function collectBindingNames(name: ts.BindingName, out: Set<string>): void {
27
+ if (ts.isIdentifier(name)) {
28
+ out.add(name.text)
29
+ return
30
+ }
31
+ for (const el of name.elements) {
32
+ if (ts.isBindingElement(el)) collectBindingNames(el.name, out)
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Names bound by `n` for its subtree, or null when `n` introduces no
38
+ * scope. Function-likes bind their parameters (and a function
39
+ * expression its own name); blocks bind their statement-level
40
+ * variable/function declarations; catch clauses bind their variable.
41
+ */
42
+ function scopeFrameOf(n: ts.Node): Set<string> | null {
43
+ if (ts.isFunctionLike(n)) {
44
+ const frame = new Set<string>()
45
+ for (const p of n.parameters) collectBindingNames(p.name, frame)
46
+ if ((ts.isFunctionExpression(n) || ts.isFunctionDeclaration(n)) && n.name) frame.add(n.name.text)
47
+ return frame.size > 0 ? frame : null
48
+ }
49
+ if (ts.isBlock(n)) {
50
+ const frame = new Set<string>()
51
+ for (const st of n.statements) {
52
+ if (ts.isVariableStatement(st)) {
53
+ for (const d of st.declarationList.declarations) collectBindingNames(d.name, frame)
54
+ } else if (ts.isFunctionDeclaration(st) && st.name) {
55
+ frame.add(st.name.text)
56
+ }
57
+ }
58
+ return frame.size > 0 ? frame : null
59
+ }
60
+ if (ts.isCatchClause(n) && n.variableDeclaration) {
61
+ const frame = new Set<string>()
62
+ collectBindingNames(n.variableDeclaration.name, frame)
63
+ return frame.size > 0 ? frame : null
64
+ }
65
+ return null
66
+ }
67
+
68
+ /**
69
+ * Depth-first walk that maintains the binding stack described above and
70
+ * reports every identifier along with whether a binding within `root`
71
+ * currently shadows it.
72
+ */
73
+ function walkWithScope(
74
+ root: ts.Node,
75
+ visit: (ident: ts.Identifier, parent: ts.Node | undefined, shadowed: boolean) => void,
76
+ ): void {
77
+ const scopeStack: Set<string>[] = []
78
+ const isShadowed = (name: string) => scopeStack.some(frame => frame.has(name))
79
+ function rec(n: ts.Node, parent?: ts.Node) {
80
+ const frame = scopeFrameOf(n)
81
+ if (frame) scopeStack.push(frame)
82
+ if (ts.isIdentifier(n)) visit(n, parent, isShadowed(n.text))
83
+ ts.forEachChild(n, child => rec(child, n))
84
+ if (frame) scopeStack.pop()
85
+ }
86
+ rec(root)
87
+ }
88
+
89
+ /**
90
+ * True when `n` sits in a non-value position where a prop rewrite must
91
+ * never apply: an object-literal key, a member-access name, a binding
92
+ * position (parameter / variable / binding-element name), or a type
93
+ * reference.
94
+ */
95
+ function isNonValuePosition(n: ts.Identifier, parent: ts.Node | undefined): boolean {
96
+ if (!parent) return false
97
+ if (ts.isPropertyAssignment(parent) && parent.name === n) return true
98
+ if (ts.isPropertyAccessExpression(parent) && parent.name === n) return true
99
+ if (ts.isQualifiedName(parent) && parent.right === n) return true
100
+ if ((ts.isParameter(parent) || ts.isVariableDeclaration(parent) || ts.isBindingElement(parent)) && parent.name === n) return true
101
+ if (ts.isTypeReferenceNode(parent)) return true
102
+ return false
103
+ }
104
+
13
105
  /**
14
106
  * Walk an AST node for destructured-prop value references and add
15
107
  * each found name to `out`. Same skip rules as the rewrite path —
16
- * object-literal keys, shorthand properties, and property-access
17
- * names are excluded so only true value references get picked up.
18
- * Exported for callers that need the raw discovery set (e.g. the
19
- * branch-local prop-dep cache from #1425).
108
+ * object-literal keys, shorthand properties, property-access names,
109
+ * and names shadowed by a binding inside `node` are excluded so only
110
+ * true value references get picked up. Exported for callers that need
111
+ * the raw discovery set (e.g. the branch-local prop-dep cache from
112
+ * #1425).
20
113
  */
21
114
  export function collectAstPropRefs(
22
115
  node: ts.Node,
23
116
  propNames: Set<string>,
24
117
  out: Set<string>,
25
118
  ): void {
26
- function visit(n: ts.Node, parent?: ts.Node) {
27
- if (ts.isIdentifier(n) && propNames.has(n.text)) {
28
- // Skip: object literal key { org: ... }
29
- if (parent && ts.isPropertyAssignment(parent) && parent.name === n) return
30
- // Skip: shorthand property { org }
31
- if (parent && ts.isShorthandPropertyAssignment(parent) && parent.name === n) return
32
- // Skip: property access name foo.org
33
- if (parent && ts.isPropertyAccessExpression(parent) && parent.name === n) return
34
- out.add(n.text)
119
+ walkWithScope(node, (n, parent, shadowed) => {
120
+ if (shadowed || !propNames.has(n.text)) return
121
+ if (parent && ts.isShorthandPropertyAssignment(parent) && parent.name === n) return
122
+ if (isNonValuePosition(n, parent)) return
123
+ out.add(n.text)
124
+ })
125
+ }
126
+
127
+ /**
128
+ * Scope-aware rewrite: parse `text` as an expression, walk it with the
129
+ * binding stack, and splice `${PROPS_PARAM}.` onto exactly the
130
+ * identifier references that are (a) in `propRefs` and (b) not
131
+ * shadowed by a binding inside the text. Shorthand properties expand
132
+ * (`{ org }` → `{ org: _p.org }`) so the result stays syntactically
133
+ * valid.
134
+ *
135
+ * Returns null when `text` does not parse cleanly as an expression —
136
+ * the caller falls back to the legacy regex rewrite.
137
+ */
138
+ function applyScopedPropRefRewrite(text: string, propRefs: Set<string>): string | null {
139
+ // Wrap in parens so object literals and arrows parse as expressions.
140
+ const prefix = '('
141
+ const sf = ts.createSourceFile('__bf_prop_rewrite.ts', `${prefix}${text}\n)`, ts.ScriptTarget.Latest, true)
142
+ const parseDiagnostics = (sf as unknown as { parseDiagnostics?: unknown[] }).parseDiagnostics
143
+ if (parseDiagnostics && parseDiagnostics.length > 0) return null
144
+
145
+ const edits: Array<{ start: number; end: number; replacement: string }> = []
146
+ walkWithScope(sf, (n, parent, shadowed) => {
147
+ if (shadowed || !propRefs.has(n.text)) return
148
+ if (isNonValuePosition(n, parent)) return
149
+ const start = n.getStart(sf) - prefix.length
150
+ const end = n.getEnd() - prefix.length
151
+ if (start < 0 || end > text.length) return
152
+ if (parent && ts.isShorthandPropertyAssignment(parent) && parent.name === n) {
153
+ edits.push({ start, end, replacement: `${n.text}: ${PROPS_PARAM}.${n.text}` })
154
+ return
35
155
  }
36
- ts.forEachChild(n, child => visit(child, n))
156
+ edits.push({ start, end, replacement: `${PROPS_PARAM}.${n.text}` })
157
+ })
158
+
159
+ if (edits.length === 0) return text
160
+ let result = text
161
+ for (const edit of edits.sort((a, b) => b.start - a.start)) {
162
+ result = result.slice(0, edit.start) + edit.replacement + result.slice(edit.end)
37
163
  }
38
- visit(node)
164
+ return result
39
165
  }
40
166
 
41
167
  /**
@@ -43,6 +169,10 @@ export function collectAstPropRefs(
43
169
  * type-stripped expression text. Idempotent under `_p.X` (negative
44
170
  * lookbehind on `_p\\.`) and skips object-literal keys via the
45
171
  * post-match `{,` + `:` shape check.
172
+ *
173
+ * Legacy fallback for text that doesn't parse as a standalone
174
+ * expression — it cannot see scopes, so `applyScopedPropRefRewrite`
175
+ * is always tried first.
46
176
  */
47
177
  export function applyRegexPropRefRewrite(
48
178
  text: string,
@@ -77,8 +207,8 @@ export function applyRegexPropRefRewrite(
77
207
  * @param extraPropRefs - Optional prop names known to appear in
78
208
  * `text` via substitution sources the AST walk can't see (e.g.
79
209
  * `text` was produced by inlining a branch-local whose initializer
80
- * references the prop). The post-match `_p\\.` lookbehind keeps
81
- * the regex idempotent, so passing an over-broad set is safe.
210
+ * references the prop). The rewrite only touches genuine value
211
+ * references, so passing an over-broad set is safe.
82
212
  */
83
213
  export function rewriteBarePropRefs(
84
214
  text: string,
@@ -95,5 +225,5 @@ export function rewriteBarePropRefs(
95
225
  }
96
226
  }
97
227
  if (foundPropRefs.size === 0) return undefined
98
- return applyRegexPropRefRewrite(text, foundPropRefs)
228
+ return applyScopedPropRefRewrite(text, foundPropRefs) ?? applyRegexPropRefRewrite(text, foundPropRefs)
99
229
  }
@@ -69,13 +69,12 @@ type EvidenceMetadata = Pick<IRMetadata, 'propsType' | 'propsObjectName' | 'prop
69
69
  * check callers gate on).
70
70
  */
71
71
  function isNullishArm(t: TypeInfo): boolean {
72
- if (t.kind === 'primitive' && (t.primitive === 'null' || t.primitive === 'undefined')) return true
73
- // `null` as a type annotation is a `ts.LiteralTypeNode` (not the
74
- // `NullKeyword` `typeNodeToTypeInfo`'s primitive switch checks for), so it
75
- // falls through to `{ kind: 'unknown', raw: 'null' }` there — a pre-existing
76
- // gap in that shared helper, out of scope to fix here. Match on `raw` too
77
- // so `Date | null` still strips down to `Date`.
78
- return t.kind === 'unknown' && (t.raw === 'null' || t.raw === 'undefined')
72
+ // Purely structural: `typeNodeToTypeInfo`'s literal-type arm lowers a
73
+ // `null` union member (`ts.LiteralTypeNode(NullKeyword)`) to
74
+ // `{ kind: 'primitive', primitive: 'null' }`, the same shape a bare
75
+ // `null` annotation always produced the raw-text fallback this helper
76
+ // used to carry for the analyzer gap is gone with the gap.
77
+ return t.kind === 'primitive' && (t.primitive === 'null' || t.primitive === 'undefined')
79
78
  }
80
79
 
81
80
  export function stripUnion(type: TypeInfo | null): TypeInfo | null {
@@ -107,15 +107,20 @@ export function extractSsrDefaults(metadata: IRMetadata): Record<string, SsrDefa
107
107
  // wins via `propName`).
108
108
  for (const p of metadata.propsParams) {
109
109
  if (p.isRest) continue
110
+ // `propName` is the CALLER-facing key (`$props->{propName}` in the Perl
111
+ // consumer above) — for a renaming destructure (`{ n: count }`) that's
112
+ // `n`, not the local binding `count` the template variable is keyed by.
113
+ // `sourceName ?? name` is an identity for every un-aliased prop.
114
+ const callerPropName = p.sourceName ?? p.name
110
115
  if (metadata.propsObjectName === null && p.defaultValue !== undefined) {
111
116
  const value = tryStaticEval(p.defaultValue, { bindings: {}, propsLike })
112
- out[p.name] = { propName: p.name, value: resultToJsonable(value) }
117
+ out[p.name] = { propName: callerPropName, value: resultToJsonable(value) }
113
118
  } else {
114
119
  // No destructure default — the template reads the prop as-is;
115
120
  // Perl's `//` operator decides what `undef` becomes. The entry
116
121
  // still matters so the template variable exists and consumers
117
122
  // can supply the propName even with no static fallback.
118
- out[p.name] = { propName: p.name, value: null }
123
+ out[p.name] = { propName: callerPropName, value: null }
119
124
  }
120
125
  }
121
126
  // Rest-props bag (`...props`) — tracked separately from `propsParams`
@@ -393,8 +393,14 @@ function deriveFormat(locale: string, probeOptions: Record<string, string>): Loc
393
393
  */
394
394
  /** A quoted string-literal union member's value, or null when the member is anything else. */
395
395
  function unionMemberLiteral(member: TypeInfo): string | null {
396
- const m = /^'([^'\\]*)'$|^"([^"\\]*)"$/.exec(member.raw.trim())
397
- return m ? (m[1] ?? m[2]) : null
396
+ // Structural since the analyzer's literal-type arm: a string-literal
397
+ // member is `kind: 'primitive', primitive: 'string'` carrying its value
398
+ // in `literalValue`. (Previously regexed `member.raw` because literal
399
+ // members lowered to `kind: 'unknown'`.)
400
+ if (member.kind === 'primitive' && member.primitive === 'string' && member.literalValue !== undefined) {
401
+ return member.literalValue
402
+ }
403
+ return null
398
404
  }
399
405
 
400
406
  /**
package/src/types.ts CHANGED
@@ -60,6 +60,13 @@ export interface TypeInfo {
60
60
  // For primitives
61
61
  primitive?: 'string' | 'number' | 'boolean' | 'null' | 'undefined'
62
62
 
63
+ /** Set when the type is a LITERAL of its primitive family (`'a'`, `42`,
64
+ * `true`) — the literal's source value. `typeNodeToTypeInfo` lowers a
65
+ * literal type to `kind: 'primitive'` + this field, so family-only
66
+ * consumers need no special case and value consumers never re-parse
67
+ * `raw`. */
68
+ literalValue?: string
69
+
63
70
  // For objects/interfaces
64
71
  properties?: PropertyInfo[]
65
72
 
@@ -854,6 +861,28 @@ export interface MapCallbackPreamble {
854
861
  ssrText: TsxSourceText
855
862
  /** const/let/function names the preamble declares (D5 key-derivability guard). */
856
863
  declaredNames: string[]
864
+ /**
865
+ * The whole preamble as backend-neutral value declarations — set only when
866
+ * EVERY statement is one (#2447). This is the DSL SSR carrier: a template
867
+ * language cannot execute `segments` (that is JS text, for the client bundle
868
+ * and for JS-runtime SSR), but it CAN declare a per-row local, and each
869
+ * declaration's initializer is a {@link ParsedExpr} every adapter already
870
+ * knows how to emit in its own syntax — the same door `arrayParsed` /
871
+ * `filterPredicate.predicate` / `sortComparator` go through.
872
+ *
873
+ * All-or-nothing on purpose. A preamble is a statement SEQUENCE with
874
+ * order-dependent scope; lowering the declarable prefix and dropping the rest
875
+ * would leave later statements' effects missing with no diagnostic — the
876
+ * silent divergence #2447 was. So one non-declaration statement (an
877
+ * assignment, a loop, a call for side effects) or one initializer the
878
+ * expression subset cannot model leaves this `undefined`, and the Phase-1
879
+ * DSL gate refuses the whole loop with `/* @client *\/` guidance.
880
+ *
881
+ * Declarations appear in source order and may read earlier ones; adapters
882
+ * emit them in order, so an earlier local is in scope for a later
883
+ * initializer, exactly as in the source.
884
+ */
885
+ declarations?: PreambleValueDeclaration[]
857
886
  /**
858
887
  * The subset of {@link declaredNames} that accumulate JSX leaves — the
859
888
  * `push`/`unshift` targets of a `jsx` segment and the declaration targets of
@@ -864,6 +893,25 @@ export interface MapCallbackPreamble {
864
893
  builderNames: string[]
865
894
  }
866
895
 
896
+ /**
897
+ * One `const`/`let` value declaration from a `.map()` callback preamble, in
898
+ * backend-neutral form (#2447 — see {@link MapCallbackPreamble.declarations}).
899
+ * `name` is always a simple identifier: a destructuring binding declares
900
+ * several names from one initializer, which no adapter has a single per-row
901
+ * local form for, so that shape refuses instead of being carried here.
902
+ */
903
+ export interface PreambleValueDeclaration {
904
+ /** Declared local name (simple identifier binding). */
905
+ name: string
906
+ /** Initializer, structured for per-adapter emission. */
907
+ valueParsed: ParsedExpr
908
+ /**
909
+ * Initializer source text, for a diagnostic that needs to quote it. Never an
910
+ * emission input — emission goes through `valueParsed`.
911
+ */
912
+ raw: string
913
+ }
914
+
867
915
  /**
868
916
  * One loop-body expression child classified as a preamble-patched region
869
917
  * (#2389 — see `IRLoop.preambleRegions`). `expr` is the raw (unwrapped)
@@ -1002,6 +1050,20 @@ export interface IRComponent {
1002
1050
  * Omitted (undefined) for ordinary components to keep IR diffs minimal.
1003
1051
  */
1004
1052
  dynamicTag?: boolean
1053
+ /**
1054
+ * True when this component is a DIRECT member of an `IRLoop.children`
1055
+ * array (the loop's row-root component, e.g. `{rows().map(r => <Row/>)}`).
1056
+ * Such a component owns its own per-row identity and gets a freshly
1057
+ * randomized `bf-s` — it does NOT derive its scope id from the parent
1058
+ * scope + mount slot the way an ordinarily-slotted child does.
1059
+ *
1060
+ * A component nested *below* the row root (e.g. `<li><Badge/></li>` where
1061
+ * `<li>` is the loop row root) is NOT marked — it derives its scope id
1062
+ * from parent scope + slot like any other slotted child. See
1063
+ * `derivesScopeFromSlot` in `./adapters/child-scope.ts`, which is the one
1064
+ * predicate every backend must consult instead of re-deriving this fact.
1065
+ */
1066
+ loopItemRoot?: boolean
1005
1067
  loc: SourceLocation
1006
1068
  }
1007
1069
 
@@ -1399,6 +1461,15 @@ export interface SignalInfo {
1399
1461
  * AST-level pre-rewrite).
1400
1462
  */
1401
1463
  templateInitialValue?: string
1464
+ /**
1465
+ * True for the getter-elided declaration form `const [, setX] =
1466
+ * createSignal(...)`. `getter` then holds a synthesized internal name
1467
+ * (`__bfGet_<setter>`) that no expression ever references — it exists so
1468
+ * getter-keyed consumers (substitution env, SSR seeding) stay total.
1469
+ * Emit reproduces the source's `[, setter]` hole instead of introducing
1470
+ * the synthesized name into output.
1471
+ */
1472
+ getterElided?: boolean
1402
1473
  type: TypeInfo
1403
1474
  loc: SourceLocation
1404
1475
  /**
@@ -1458,6 +1529,15 @@ export interface SignalInfo {
1458
1529
  export interface MemoInfo {
1459
1530
  name: string
1460
1531
  computation: string
1532
+ /**
1533
+ * `computation` with bare destructured-prop refs rewritten to `_p.X` —
1534
+ * the memo twin of `SignalInfo.templateInitialValue` (#2265). Consumed by
1535
+ * `buildSignalMemoEnv` when the memo body is inlined into the module-scope
1536
+ * CSR `template:` arrow, which is not a closure over init's destructured
1537
+ * extractions (#2468). Only set for destructured-arg components that
1538
+ * actually reference a prop.
1539
+ */
1540
+ templateComputation?: string
1461
1541
  /** Computation with TypeScript type annotations preserved, for .tsx output */
1462
1542
  typedComputation?: string
1463
1543
  /**