@barefootjs/jsx 0.35.0 → 0.35.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 (72) hide show
  1. package/dist/analyzer-context.d.ts.map +1 -1
  2. package/dist/index.js +731 -691
  3. package/dist/ir-to-client-js/collect-elements.d.ts +22 -2
  4. package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
  5. package/dist/ir-to-client-js/control-flow/plan/build-composite-loop.d.ts.map +1 -1
  6. package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts +14 -2
  7. package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts.map +1 -1
  8. package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts.map +1 -1
  9. package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts +6 -1
  10. package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts.map +1 -1
  11. package/dist/ir-to-client-js/control-flow/plan/build-plain-row.d.ts.map +1 -1
  12. package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts +3 -2
  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/inner-loop.d.ts +24 -44
  15. package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts.map +1 -1
  16. package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts +14 -0
  17. package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts.map +1 -1
  18. package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts +6 -14
  19. package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts.map +1 -1
  20. package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts +1 -3
  21. package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts.map +1 -1
  22. package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts +19 -3
  23. package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
  24. package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
  25. package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts +5 -1
  26. package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
  27. package/dist/ir-to-client-js/emit-reactive.d.ts +54 -0
  28. package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
  29. package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
  30. package/dist/ir-to-client-js/reactivity.d.ts +62 -18
  31. package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
  32. package/dist/ir-to-client-js/types.d.ts +18 -2
  33. package/dist/ir-to-client-js/types.d.ts.map +1 -1
  34. package/dist/reactivity-checker.d.ts +18 -0
  35. package/dist/reactivity-checker.d.ts.map +1 -1
  36. package/package.json +2 -2
  37. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +128 -10
  38. package/src/__tests__/binding-scope-ratchet.test.ts +26 -3
  39. package/src/__tests__/child-prop-fallback-wrap.test.ts +4 -2
  40. package/src/__tests__/compiler-stress-1244.test.ts +8 -8
  41. package/src/__tests__/issue-2868-cond-arm-tag-collision.test.ts +185 -0
  42. package/src/__tests__/issue-2869-attr-dedup-guard.test.ts +141 -0
  43. package/src/__tests__/loop-index-reactivity.test.ts +208 -0
  44. package/src/__tests__/map-body-brand-checker.test.ts +176 -0
  45. package/src/__tests__/nested-loop-index-param.test.ts +27 -5
  46. package/src/__tests__/nested-loop-non-outer-reactivity.test.ts +177 -0
  47. package/src/__tests__/nested-loop-plain.test.ts +39 -25
  48. package/src/__tests__/nested-loop-reactive-attrs.test.ts +12 -4
  49. package/src/__tests__/preamble-branch-arm-rerun.test.ts +135 -0
  50. package/src/__tests__/style-css-var-reactive.test.ts +4 -1
  51. package/src/analyzer-context.ts +12 -10
  52. package/src/ir-to-client-js/collect-elements.ts +110 -32
  53. package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +2 -0
  54. package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +28 -51
  55. package/src/ir-to-client-js/control-flow/plan/build-lazy-row.ts +7 -2
  56. package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +33 -6
  57. package/src/ir-to-client-js/control-flow/plan/build-plain-row.ts +7 -1
  58. package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +18 -6
  59. package/src/ir-to-client-js/control-flow/plan/inner-loop.ts +24 -45
  60. package/src/ir-to-client-js/control-flow/plan/loop-child-arm.ts +14 -0
  61. package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +20 -66
  62. package/src/ir-to-client-js/control-flow/stringify/insert.ts +4 -2
  63. package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +12 -15
  64. package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +53 -11
  65. package/src/ir-to-client-js/control-flow/stringify/loop.ts +4 -2
  66. package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +29 -18
  67. package/src/ir-to-client-js/emit-reactive.ts +88 -5
  68. package/src/ir-to-client-js/html-template.ts +9 -1
  69. package/src/ir-to-client-js/reactivity.ts +73 -27
  70. package/src/ir-to-client-js/types.ts +19 -3
  71. package/src/jsx-to-ir.ts +21 -4
  72. package/src/reactivity-checker.ts +60 -0
@@ -400,7 +400,19 @@ export interface LoopChildRef {
400
400
  export interface BranchLoop extends LoopCore {
401
401
  kind: 'branch'
402
402
  index: string | null // Index parameter (e.g., 'i')
403
- template: string // HTML template for each item
403
+ template: string // HTML template for each item, item-only wrapped (no index — see `templateIndexed`)
404
+ /**
405
+ * Same render as `template`, but with `index` also wrapped as an accessor
406
+ * (`i()`) — rendered structurally at IR time (`irToHtmlTemplate`'s
407
+ * `loopParams`), never via a post-hoc regex pass over `template` (#2868:
408
+ * a word-boundary regex over assembled HTML can match a bare tag name
409
+ * colliding with the index identifier, e.g. `<i>` -> `<i()>`). Present
410
+ * iff the loop declares an index; `build-plain-row.ts` picks this over
411
+ * `template` once lazy eligibility rules out the lazy row plan (which
412
+ * needs the index UNWRAPPED — `mapArrayLazy`'s `createRow` hands it a
413
+ * plain number, never an accessor).
414
+ */
415
+ templateIndexed?: string
404
416
  containerSlotId: string // bf slot ID of the container element (e.g., 's1' for <ul bf="s1">)
405
417
  preamble?: MapCallbackPreamble
406
418
  // Composite loop fields (loops whose body contains child components)
@@ -489,8 +501,6 @@ export interface NestedLoop extends LoopCore {
489
501
  * (and any subsequent reads) are in scope (#1052).
490
502
  */
491
503
  preamble?: MapCallbackPreamble
492
- /** Whether the inner array references the outer loop param (needs reactive mapArray) */
493
- refsOuterParam?: boolean
494
504
  /** Child components inside inner loop items (for initChild/createComponent) */
495
505
  childComponents?: import('../types.ts').IRLoopChildComponent[]
496
506
  /** True when this loop is inside a conditional branch (handled by insert() bindEvents instead) */
@@ -622,6 +632,12 @@ export interface TopLevelLoop extends LoopCore {
622
632
  slotId: string
623
633
  index: string | null
624
634
  template: string
635
+ /**
636
+ * Same render as `template`, but with `index` also wrapped as an accessor
637
+ * (`i()`) — see `BranchLoop.templateIndexed` for why this exists as a
638
+ * second structured render rather than a post-hoc regex pass (#2868).
639
+ */
640
+ templateIndexed?: string
625
641
  /**
626
642
  * Per-iteration HTML template for static-array loops that need to
627
643
  * self-heal on CSR mount (#1247). Unlike `template`, this variant skips
package/src/jsx-to-ir.ts CHANGED
@@ -45,7 +45,7 @@ import type { IRLoopSort, FunctionInfo, ConstantInfo } from './types.ts'
45
45
  import { formatParamWithType } from './module-exports.ts'
46
46
  import { createError, ErrorCodes, internalInvariant } from './errors.ts'
47
47
  import { CLIENT_BUILTIN_SOURCE, isClientBuiltinName, type ClientBuiltinTag } from './builtins.ts'
48
- import { containsReactiveExpression } from './reactivity-checker.ts'
48
+ import { containsReactiveExpression, collectReactiveBrandLeaves } from './reactivity-checker.ts'
49
49
  import {
50
50
  rewriteBarePropRefs as rewriteBarePropRefsCore,
51
51
  collectAstPropRefs,
@@ -2429,7 +2429,13 @@ function transformExpressionInner(
2429
2429
  // A reactive brand-package condition (e.g. `form.field('x').error() &&
2430
2430
  // …`) can't be SSR-evaluated, so defer the whole conditional rather
2431
2431
  // than raising BF061 — same routing as a manual `/* @client */` (#1638).
2432
- if ((isClientOnly || shouldAutoDeferReactiveBrand(expr, ctx)) && ir.type === 'conditional') {
2432
+ // Check `ir.type` first: `shouldAutoDeferReactiveBrand` only matters for
2433
+ // IRConditional, but walks the checker over `expr` — for every other
2434
+ // shape (in particular IRLoop, whose `expr` is a `.map()` callback body
2435
+ // full of JSX) that walk is both wasted work and, historically, exactly
2436
+ // what tripped the getJS trust-boundary assertion on a JSX-bearing node
2437
+ // (#2867/#2830).
2438
+ if (ir.type === 'conditional' && (isClientOnly || shouldAutoDeferReactiveBrand(expr, ctx))) {
2433
2439
  ir.clientOnly = true
2434
2440
  if (!ir.slotId) {
2435
2441
  ir.slotId = generateSlotId(ctx)
@@ -8210,10 +8216,21 @@ function isReactiveExpression(expr: string, ctx: TransformContext, astNode?: ts.
8210
8216
  function shouldAutoDeferReactiveBrand(expr: ts.Expression, ctx: TransformContext): boolean {
8211
8217
  const checker = ctx.analyzer.checker
8212
8218
  if (!checker) return false
8213
- if (!containsReactiveExpression(expr, checker)) return false
8219
+ // Read only the brand-carrying leaf(s), never `expr` itself: `expr` can be
8220
+ // a `.map()` callback or a ternary/`&&` with JSX arms, and getJS() refuses
8221
+ // to stringify a JSX-bearing node (the write-side trust boundary — JSX
8222
+ // must travel as structured segments, never raw text). Each leaf is an
8223
+ // identifier / property access / call callee, none of which can contain
8224
+ // JSX, so getJS() is safe on them regardless of what the rest of `expr`
8225
+ // contains (#2867/#2830 — this used to call getJS(expr) directly and
8226
+ // throw whenever the checker was active and expr had JSX in it).
8227
+ const leaves = collectReactiveBrandLeaves(expr, checker)
8228
+ if (leaves.length === 0) return false
8214
8229
  // Native signals/memos (incl. chained-const aliases) are SSR-derivable —
8215
8230
  // leave them to the normal template path so their initial value renders.
8216
- if (isSignalOrMemoReference(ctx.getJS(expr), ctx)) return false
8231
+ // Preserves the original "any native ref anywhere defers to non-deferred"
8232
+ // semantics by checking every reactive-brand leaf, not just the first.
8233
+ if (leaves.some((leaf) => isSignalOrMemoReference(ctx.getJS(leaf), ctx))) return false
8217
8234
  return true
8218
8235
  }
8219
8236
 
@@ -191,3 +191,63 @@ export function containsReactiveExpression(node: ts.Node, checker: ts.TypeChecke
191
191
  incrementCounter('reactivityChecks')
192
192
  return brandTypeReactivityAnalyzer.analyze(node, checker).isReactive
193
193
  }
194
+
195
+ /** Subtree JSX check — also used by analyzer-context.ts's getJS() guard. */
196
+ export function nodeContainsJsx(node: ts.Node): boolean {
197
+ if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node)) return true
198
+ return ts.forEachChild(node, nodeContainsJsx) ?? false
199
+ }
200
+
201
+ /**
202
+ * Every node that itself carries the `Reactive<T>` brand within `node`,
203
+ * without stopping at the first match (unlike `analyze`, which only needs
204
+ * one hit to answer "is this reactive"). A caller that needs to read the
205
+ * *text* of the reactive part(s) — e.g. to tell a brand-package wrapper
206
+ * apart from a plain native signal/memo call (#2867) — must not stringify
207
+ * `node` itself: for a `.map()` callback or a ternary with JSX arms, `node`
208
+ * is JSX-bearing, and the write-side trust boundary (`getJS()`,
209
+ * `analyzer-context.ts`) rejects producing text for a JSX-bearing node.
210
+ * Each returned node is JSX-free: property access and identifier leaves
211
+ * cannot contain JSX by construction; a call leaf is the whole call
212
+ * (preserving the `name(...)` shape native-signal/memo regex patterns
213
+ * match against) unless its arguments happen to contain JSX, in which case
214
+ * only the callee is returned instead.
215
+ */
216
+ export function collectReactiveBrandLeaves(node: ts.Node, checker: ts.TypeChecker): ts.Node[] {
217
+ const leaves: ts.Node[] = []
218
+ const visit = (n: ts.Node): void => {
219
+ if (ts.isPropertyAccessExpression(n)) {
220
+ try {
221
+ if (isReactiveType(queryType(checker, n))) {
222
+ leaves.push(n)
223
+ return
224
+ }
225
+ } catch {
226
+ // fall through to walking the object part
227
+ }
228
+ visit(n.expression)
229
+ return
230
+ }
231
+ if (ts.isIdentifier(n)) {
232
+ try {
233
+ if (isReactiveType(queryType(checker, n))) leaves.push(n)
234
+ } catch {
235
+ // not reactive
236
+ }
237
+ return
238
+ }
239
+ if (ts.isCallExpression(n)) {
240
+ try {
241
+ if (isReactiveType(queryType(checker, n.expression))) {
242
+ leaves.push(nodeContainsJsx(n) ? n.expression : n)
243
+ return
244
+ }
245
+ } catch {
246
+ // fall through to walking children
247
+ }
248
+ }
249
+ ts.forEachChild(n, visit)
250
+ }
251
+ visit(node)
252
+ return leaves
253
+ }