@barefootjs/jsx 0.33.3 → 0.33.6

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 (87) hide show
  1. package/dist/adapters/child-scope.d.ts +13 -0
  2. package/dist/adapters/child-scope.d.ts.map +1 -1
  3. package/dist/analyzer.d.ts.map +1 -1
  4. package/dist/errors.d.ts +1 -0
  5. package/dist/errors.d.ts.map +1 -1
  6. package/dist/index.d.ts +2 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1392 -1117
  9. package/dist/ir-to-client-js/build-references.d.ts.map +1 -1
  10. package/dist/ir-to-client-js/child-components.d.ts.map +1 -1
  11. package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
  12. package/dist/ir-to-client-js/component-scope.d.ts +20 -0
  13. package/dist/ir-to-client-js/component-scope.d.ts.map +1 -1
  14. package/dist/ir-to-client-js/compute-inlinability.d.ts.map +1 -1
  15. package/dist/ir-to-client-js/control-flow/plan/build-component-loop.d.ts.map +1 -1
  16. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +2 -4
  17. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
  18. package/dist/ir-to-client-js/control-flow/shared.d.ts +4 -3
  19. package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
  20. package/dist/ir-to-client-js/control-flow/stringify/component-loop.d.ts.map +1 -1
  21. package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
  22. package/dist/ir-to-client-js/control-flow.d.ts.map +1 -1
  23. package/dist/ir-to-client-js/csr-substitute.d.ts +35 -9
  24. package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
  25. package/dist/ir-to-client-js/emit-reactive.d.ts +10 -0
  26. package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
  27. package/dist/ir-to-client-js/html-template.d.ts +49 -2
  28. package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
  29. package/dist/ir-to-client-js/imports.d.ts +2 -2
  30. package/dist/ir-to-client-js/imports.d.ts.map +1 -1
  31. package/dist/ir-to-client-js/index.d.ts.map +1 -1
  32. package/dist/ir-to-client-js/rewrite-props-object.d.ts +12 -1
  33. package/dist/ir-to-client-js/rewrite-props-object.d.ts.map +1 -1
  34. package/dist/jsx-to-ir.d.ts.map +1 -1
  35. package/dist/prop-rewrite.d.ts +5 -2
  36. package/dist/prop-rewrite.d.ts.map +1 -1
  37. package/dist/props-binding.d.ts +49 -11
  38. package/dist/props-binding.d.ts.map +1 -1
  39. package/dist/types.d.ts +19 -5
  40. package/dist/types.d.ts.map +1 -1
  41. package/package.json +2 -2
  42. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +15 -15
  43. package/src/__tests__/aliased-import-child-registry-2777.test.ts +112 -0
  44. package/src/__tests__/date-lowering.test.ts +18 -4
  45. package/src/__tests__/fragment-body-loop-key.test.ts +95 -0
  46. package/src/__tests__/inner-loop-mathml-namespace.test.ts +1 -1
  47. package/src/__tests__/inner-loop-svg-namespace.test.ts +4 -4
  48. package/src/__tests__/issue-2724-prop-alias-loop-not-static.test.ts +355 -0
  49. package/src/__tests__/issue-2737-props-object-alias-template.test.ts +86 -0
  50. package/src/__tests__/issue-2756-input-skeleton-bails.test.ts +123 -0
  51. package/src/__tests__/issue-2778-getter-alias-csr-template.test.ts +113 -0
  52. package/src/__tests__/issue-2806-rest-bag-template-rewrite.test.ts +136 -0
  53. package/src/__tests__/issue-2835-props-type-member-name-collision.test.ts +259 -0
  54. package/src/__tests__/loop-item-root-scope.test.ts +7 -3
  55. package/src/__tests__/markup-prop-brand.test.ts +165 -26
  56. package/src/__tests__/multi-root-loop-body.test.ts +7 -3
  57. package/src/__tests__/namespace-import-primitive.test.ts +244 -0
  58. package/src/__tests__/preamble-declarations.test.ts +42 -0
  59. package/src/__tests__/rewrite-destructured-props.test.ts +53 -0
  60. package/src/__tests__/static-loop-csr-materialize.test.ts +2 -2
  61. package/src/adapters/child-scope.ts +23 -0
  62. package/src/analyzer.ts +130 -0
  63. package/src/errors.ts +14 -1
  64. package/src/index.ts +7 -0
  65. package/src/ir-to-client-js/build-references.ts +7 -0
  66. package/src/ir-to-client-js/child-components.ts +8 -1
  67. package/src/ir-to-client-js/collect-elements.ts +10 -38
  68. package/src/ir-to-client-js/component-scope.ts +60 -0
  69. package/src/ir-to-client-js/compute-inlinability.ts +46 -11
  70. package/src/ir-to-client-js/control-flow/plan/build-component-loop.ts +19 -1
  71. package/src/ir-to-client-js/control-flow/plan/loop.ts +2 -4
  72. package/src/ir-to-client-js/control-flow/shared.ts +12 -5
  73. package/src/ir-to-client-js/control-flow/stringify/component-loop.ts +7 -0
  74. package/src/ir-to-client-js/control-flow/stringify/loop.ts +8 -2
  75. package/src/ir-to-client-js/control-flow.ts +12 -0
  76. package/src/ir-to-client-js/csr-substitute.ts +82 -16
  77. package/src/ir-to-client-js/emit-reactive.ts +29 -13
  78. package/src/ir-to-client-js/emit-registration.ts +1 -1
  79. package/src/ir-to-client-js/html-template.ts +194 -82
  80. package/src/ir-to-client-js/imports.ts +4 -0
  81. package/src/ir-to-client-js/index.ts +34 -23
  82. package/src/ir-to-client-js/plan/build-static-array-child-init.ts +7 -2
  83. package/src/ir-to-client-js/rewrite-props-object.ts +14 -0
  84. package/src/jsx-to-ir.ts +318 -54
  85. package/src/prop-rewrite.ts +23 -5
  86. package/src/props-binding.ts +74 -18
  87. package/src/types.ts +19 -5
@@ -132,6 +132,16 @@ export function collectAstPropRefs(
132
132
  * (`{ org }` → `{ org: _p.org }`) so the result stays syntactically
133
133
  * valid.
134
134
  *
135
+ * `replacementFor` overrides the substitution text for a value
136
+ * reference (default `${PROPS_PARAM}.<callerKey>`) — e.g. the reactive
137
+ * client-JS emit path (`emit-reactive.ts`'s `rewriteDestructuredPropsInExpr`)
138
+ * wraps it in a `(_p.x ?? <default>)` fallback for a prop with a
139
+ * destructure default. Kept as a caller override rather than a
140
+ * duplicate AST walk in that module, per the "one decision, one
141
+ * implementation" rule (CLAUDE.md) — this walk is already the one place
142
+ * that correctly distinguishes a value reference from an object-literal
143
+ * key / property-access name / shorthand property / binding name.
144
+ *
135
145
  * Returns null when `text` does not parse cleanly as an expression —
136
146
  * the caller falls back to the legacy regex rewrite.
137
147
  */
@@ -139,6 +149,7 @@ function applyScopedPropRefRewrite(
139
149
  text: string,
140
150
  propRefs: Set<string>,
141
151
  propAliases?: ReadonlyMap<string, string>,
152
+ replacementFor?: (localName: string, callerKey: string) => string,
142
153
  ): string | null {
143
154
  // Wrap in parens so object literals and arrows parse as expressions.
144
155
  const prefix = '('
@@ -157,11 +168,12 @@ function applyScopedPropRefRewrite(
157
168
  // — #2524 CSR half); the local binding (`n.text`) only survives on the
158
169
  // left of a shorthand expansion.
159
170
  const callerKey = propAliases?.get(n.text) ?? n.text
171
+ const value = replacementFor ? replacementFor(n.text, callerKey) : `${PROPS_PARAM}.${callerKey}`
160
172
  if (parent && ts.isShorthandPropertyAssignment(parent) && parent.name === n) {
161
- edits.push({ start, end, replacement: `${n.text}: ${PROPS_PARAM}.${callerKey}` })
173
+ edits.push({ start, end, replacement: `${n.text}: ${value}` })
162
174
  return
163
175
  }
164
- edits.push({ start, end, replacement: `${PROPS_PARAM}.${callerKey}` })
176
+ edits.push({ start, end, replacement: value })
165
177
  })
166
178
 
167
179
  if (edits.length === 0) return text
@@ -186,6 +198,7 @@ export function applyRegexPropRefRewrite(
186
198
  text: string,
187
199
  propRefs: Iterable<string>,
188
200
  propAliases?: ReadonlyMap<string, string>,
201
+ replacementFor?: (localName: string, callerKey: string) => string,
189
202
  ): string {
190
203
  const { protect, restore } = createTemplateAwareStringProtector()
191
204
  let result = protect(text)
@@ -193,6 +206,7 @@ export function applyRegexPropRefRewrite(
193
206
  for (const propName of propRefs) {
194
207
  // `_p` is always keyed by the caller-facing name (#2524 CSR half).
195
208
  const callerKey = propAliases?.get(propName) ?? propName
209
+ const value = replacementFor ? replacementFor(propName, callerKey) : `${PROPS_PARAM}.${callerKey}`
196
210
  const pattern = new RegExp(`(?<!${PROPS_PARAM}\\.)(?<!['"\\w.-])\\b${propName}\\b(?![a-zA-Z0-9_$])`, 'g')
197
211
  result = result.replace(pattern, (match, offset, str) => {
198
212
  // Skip object literal keys: preceded by { or , and followed by :
@@ -201,7 +215,7 @@ export function applyRegexPropRefRewrite(
201
215
  const before = str.slice(0, offset)
202
216
  if (/[{,]\s*$/.test(before)) return match
203
217
  }
204
- return `${PROPS_PARAM}.${callerKey}`
218
+ return value
205
219
  })
206
220
  }
207
221
 
@@ -225,6 +239,9 @@ export function applyRegexPropRefRewrite(
225
239
  * `_p` is always keyed by the caller-facing name (#2524 CSR half); a name
226
240
  * absent from this map emits `_p.<name>` unchanged (the un-aliased case,
227
241
  * where `sourceName ?? name` is an identity).
242
+ * @param replacementFor - Override the substitution text for a value
243
+ * reference (default `${PROPS_PARAM}.<callerKey>`) — see
244
+ * `applyScopedPropRefRewrite`'s docstring.
228
245
  */
229
246
  export function rewriteBarePropRefs(
230
247
  text: string,
@@ -232,6 +249,7 @@ export function rewriteBarePropRefs(
232
249
  propNames: Set<string>,
233
250
  extraPropRefs?: ReadonlySet<string>,
234
251
  propAliases?: ReadonlyMap<string, string>,
252
+ replacementFor?: (localName: string, callerKey: string) => string,
235
253
  ): string | undefined {
236
254
  // Walk AST to find which prop names are actually used as value references
237
255
  const foundPropRefs = new Set<string>()
@@ -243,7 +261,7 @@ export function rewriteBarePropRefs(
243
261
  }
244
262
  if (foundPropRefs.size === 0) return undefined
245
263
  return (
246
- applyScopedPropRefRewrite(text, foundPropRefs, propAliases) ??
247
- applyRegexPropRefRewrite(text, foundPropRefs, propAliases)
264
+ applyScopedPropRefRewrite(text, foundPropRefs, propAliases, replacementFor) ??
265
+ applyRegexPropRefRewrite(text, foundPropRefs, propAliases, replacementFor)
248
266
  )
249
267
  }
@@ -69,6 +69,31 @@ export function buildPropAliasMap(params: readonly ParamInfo[]): Map<string, str
69
69
  return map
70
70
  }
71
71
 
72
+ /**
73
+ * The props-parameter shape needed to answer "is `name` an actual local
74
+ * binding introduced by the props parameter" — as opposed to
75
+ * `ctx.patterns.props`, which for a whole `(props: Props)` parameter holds
76
+ * every TYPE-MEMBER name (`extractPropsFromTypeMembers`) for regex prop-
77
+ * access matching, not real bindings.
78
+ */
79
+ export interface PropsParamBindings {
80
+ propsParams: readonly ParamInfo[]
81
+ propsObjectName: string | null
82
+ }
83
+
84
+ /**
85
+ * Local names actually bound by the props parameter. Empty for a whole
86
+ * `(props: Props)` parameter: `propsParams` there is type-member names
87
+ * (nothing local to shadow with), and `props` itself is `propsObjectName`,
88
+ * checked separately by callers.
89
+ */
90
+ export function boundPropLocalNames(b: PropsParamBindings): ReadonlySet<string> {
91
+ if (b.propsObjectName !== null) return EMPTY_SET
92
+ return new Set(b.propsParams.filter(p => !p.isRest).map(p => p.name))
93
+ }
94
+
95
+ const EMPTY_SET: ReadonlySet<string> = new Set()
96
+
72
97
  /**
73
98
  * The two component bindings that forward the caller's leftover props:
74
99
  * the destructured `...rest` binding and a whole undestructured `(props)`
@@ -82,10 +107,52 @@ export interface RestSpreadBindings {
82
107
  }
83
108
 
84
109
  /**
85
- * Which of `bindings` the name `name` ultimately reaches, walking through
86
- * any bare `const x__alias = <name>` hop recorded in `constantValues`
87
- * (name initializer text). `'rest'` for the destructured rest binding,
88
- * `'props'` for the whole props object, `null` for anything else.
110
+ * Walks a bare `const x__alias = <name>` hop chain recorded in
111
+ * `constantValues` (name initializer text) until `terminal` recognizes
112
+ * the current name, returning what it recognized. `null` when the chain
113
+ * runs out (a constant whose value isn't a bare identifier — a real
114
+ * computed object — stops the walk) or degenerates into a cycle.
115
+ *
116
+ * The one alias-hop walker in the compiler, shared by every caller that
117
+ * needs "does this name ultimately reach some fixed binding" rather than
118
+ * a bespoke copy per caller — `resolveRestSpreadOriginCore` below,
119
+ * `resolveGetterAliases` (`csr-substitute.ts`, #2778), and
120
+ * `isArrayExprDirectPropRef` (`jsx-to-ir.ts`, #2724, called directly rather
121
+ * than through a `*Core`-style wrapper) are its callers, differing only in
122
+ * what `terminal` recognizes as a hit — and, for `isArrayExprDirectPropRef`,
123
+ * in also narrowing `constantValues` itself (`propAliasHopCandidates`)
124
+ * before passing it in. `terminal` is checked BEFORE the constant-value
125
+ * lookup so a name that is itself a target take priority over any
126
+ * same-named local shadowing it (preserves `resolveRestSpreadOriginCore`'s
127
+ * original hop order) — a caller whose `terminal` can itself be fooled by a
128
+ * shadowed name (e.g. a scope-unaware lookup keyed only by name) must guard
129
+ * that inside `terminal`, since this walker has no scope/binding awareness
130
+ * of its own to do it centrally.
131
+ *
132
+ * `visited` guards a constant cycle (`const a = b; const b = a`); walking
133
+ * hop by hop (not a precomputed set) is what lets a multi-hop alias
134
+ * (`const p2 = props; const p3 = p2`) resolve through every link.
135
+ */
136
+ export function resolveAliasOrigin<T>(
137
+ constantValues: ReadonlyMap<string, string | undefined>,
138
+ name: string,
139
+ terminal: (name: string) => T | null,
140
+ ): T | null {
141
+ const visited = new Set<string>()
142
+ let current: string | undefined = name.trim()
143
+ while (current !== undefined && !visited.has(current)) {
144
+ const hit = terminal(current)
145
+ if (hit !== null) return hit
146
+ visited.add(current)
147
+ current = constantValues.get(current)?.trim()
148
+ }
149
+ return null
150
+ }
151
+
152
+ /**
153
+ * Which of `bindings` the name `name` ultimately reaches. `'rest'` for the
154
+ * destructured rest binding, `'props'` for the whole props object, `null`
155
+ * for anything else.
89
156
  *
90
157
  * The single definition of "this `{...spread}` forwards the caller's
91
158
  * leftover props", shared by the two phases that must agree on it: Phase 1
@@ -96,26 +163,15 @@ export interface RestSpreadBindings {
96
163
  * `spreadAttrs({...})` merge. Two copies of the rule would let an element
97
164
  * qualify for one and not the other, which is exactly the silent-drop
98
165
  * shape #2754 reports.
99
- *
100
- * Walks hop by hop (not a precomputed set) so a multi-hop alias
101
- * (`const p2 = props; const p3 = p2`) resolves through every link;
102
- * `visited` guards a constant cycle (`const a = b; const b = a`). A
103
- * constant whose value isn't a bare identifier on the chain (a real
104
- * computed object) stops the walk, so a genuinely different spread
105
- * expression is never mistaken for the rest object.
106
166
  */
107
167
  export function resolveRestSpreadOriginCore(
108
168
  bindings: RestSpreadBindings,
109
169
  constantValues: ReadonlyMap<string, string | undefined>,
110
170
  name: string,
111
171
  ): 'rest' | 'props' | null {
112
- const visited = new Set<string>()
113
- let current: string | undefined = name.trim()
114
- while (current !== undefined && !visited.has(current)) {
172
+ return resolveAliasOrigin(constantValues, name, (current) => {
115
173
  if (bindings.restPropsName && current === bindings.restPropsName) return 'rest'
116
174
  if (bindings.propsObjectName && current === bindings.propsObjectName) return 'props'
117
- visited.add(current)
118
- current = constantValues.get(current)?.trim()
119
- }
120
- return null
175
+ return null
176
+ })
121
177
  }
package/src/types.ts CHANGED
@@ -594,7 +594,9 @@ export interface IRLoop {
594
594
  isStaticArray: boolean
595
595
 
596
596
  /**
597
- * True when the array is a direct prop reference promoted to mapArray (#1586).
597
+ * True when the array is a direct prop reference promoted to mapArray
598
+ * (#1586) — including one reached only through a bare `const x = y`
599
+ * alias-hop chain (`isArrayExprDirectPropRef`, jsx-to-ir.ts, #2724).
598
600
  * Adapters use this to include the array in JSON serialization (bf-p)
599
601
  * even though isStaticArray is false — the client's mapArray needs the
600
602
  * initial data for hydration, unlike signal-backed arrays.
@@ -936,6 +938,19 @@ export interface MapCallbackPreamble {
936
938
  * expression subset cannot model leaves this `undefined`, and the Phase-1
937
939
  * DSL gate refuses the whole loop with `/* @client *\/` guidance.
938
940
  *
941
+ * ONE exception (#2797): a declaration whose initializer is a function
942
+ * literal is ELIDED (contributes no entry here) rather than refusing the
943
+ * preamble, when EVERY read of that name outside its own binding sits
944
+ * inside an event-handler `JsxAttribute` (`/^on[A-Z]/`) value position —
945
+ * see `neutralPreambleDeclarations`'s `everyReadIsEventHandlerPropValue`
946
+ * check. Every DSL adapter's own SSR component-prop builder already skips
947
+ * an event-handler prop outright, so a name read only that way never needs
948
+ * an SSR representation; the client-JS preamble (`segments`) still carries
949
+ * the real closure. This can leave `declarations` a legally EMPTY array
950
+ * (all-declared names elided) rather than `undefined` — the Phase-1 gate
951
+ * checks falsiness, and `[]` is truthy, so an empty array reads as "fully
952
+ * declared, nothing to emit," not a refusal.
953
+ *
939
954
  * Declarations appear in source order and may read earlier ones; adapters
940
955
  * emit them in order, so an earlier local is in scope for a later
941
956
  * initializer, exactly as in the source.
@@ -1535,10 +1550,9 @@ export interface SignalInfo {
1535
1550
  * `templateCondition` fields on other IR positions. Destructured mode
1536
1551
  * only (`propsObjectName === null`); undefined when no rewrite was
1537
1552
  * needed (no destructured prop referenced) or the component is in
1538
- * object-props mode (there, `rewritePropsObjectRef`/`applyPropsRewrite`
1539
- * handle `props.x` → `_p.x` on the final joined/substituted string
1540
- * instead, since a bare `props.x` textual match works without an
1541
- * AST-level pre-rewrite).
1553
+ * object-props mode (there, `rewritePropsObjectRef` handles `props.x` →
1554
+ * `_p.x` on the final joined/substituted string instead, since a bare
1555
+ * `props.x` textual match works without an AST-level pre-rewrite).
1542
1556
  */
1543
1557
  templateInitialValue?: string
1544
1558
  /**