@barefootjs/jsx 0.31.4 → 0.31.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 (62) hide show
  1. package/dist/adapters/jsx-adapter.d.ts.map +1 -1
  2. package/dist/adapters/loop-bound-names.d.ts +18 -0
  3. package/dist/adapters/loop-bound-names.d.ts.map +1 -1
  4. package/dist/adapters/test-adapter.d.ts.map +1 -1
  5. package/dist/augment-inherited-props.d.ts +12 -2
  6. package/dist/augment-inherited-props.d.ts.map +1 -1
  7. package/dist/compiler.d.ts.map +1 -1
  8. package/dist/debug.d.ts.map +1 -1
  9. package/dist/free-refs.d.ts +11 -2
  10. package/dist/free-refs.d.ts.map +1 -1
  11. package/dist/index.d.ts +2 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +773 -649
  14. package/dist/ir-to-client-js/client-only-elision.d.ts +11 -5
  15. package/dist/ir-to-client-js/client-only-elision.d.ts.map +1 -1
  16. package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
  17. package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts.map +1 -1
  18. package/dist/ir-to-client-js/control-flow/plan/reactive-effects.d.ts +7 -0
  19. package/dist/ir-to-client-js/control-flow/plan/reactive-effects.d.ts.map +1 -1
  20. package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
  21. package/dist/ir-to-client-js/reactivity.d.ts +16 -0
  22. package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
  23. package/dist/ir-to-client-js/types.d.ts +16 -0
  24. package/dist/ir-to-client-js/types.d.ts.map +1 -1
  25. package/dist/ir-to-client-js/utils.d.ts +15 -0
  26. package/dist/ir-to-client-js/utils.d.ts.map +1 -1
  27. package/dist/module-exports.d.ts +64 -0
  28. package/dist/module-exports.d.ts.map +1 -1
  29. package/dist/scope/binding-scope.d.ts +1 -1
  30. package/dist/types.d.ts +112 -0
  31. package/dist/types.d.ts.map +1 -1
  32. package/package.json +2 -2
  33. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +117 -17
  34. package/src/__tests__/binding-scope-ratchet.test.ts +146 -21
  35. package/src/__tests__/component-type-parameters.test.ts +70 -0
  36. package/src/__tests__/csr-materialize-loop-preamble-shadow.test.ts +10 -1
  37. package/src/__tests__/doc-examples.test.ts +1 -0
  38. package/src/__tests__/free-refs.test.ts +1 -1
  39. package/src/__tests__/mutable-binding-writers.test.ts +133 -0
  40. package/src/__tests__/preamble-conditional-reactivity.test.ts +191 -0
  41. package/src/__tests__/signal-setter-updater-type.test.ts +81 -0
  42. package/src/adapters/jsx-adapter.ts +29 -3
  43. package/src/adapters/loop-bound-names.ts +18 -0
  44. package/src/adapters/test-adapter.ts +4 -1
  45. package/src/augment-inherited-props.ts +13 -1
  46. package/src/compiler.ts +18 -0
  47. package/src/debug.ts +34 -21
  48. package/src/free-refs.ts +14 -5
  49. package/src/index.ts +6 -0
  50. package/src/ir-to-client-js/client-only-elision.ts +11 -5
  51. package/src/ir-to-client-js/collect-elements.ts +17 -2
  52. package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +1 -0
  53. package/src/ir-to-client-js/control-flow/plan/reactive-effects.ts +7 -0
  54. package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +12 -2
  55. package/src/ir-to-client-js/html-template.ts +51 -0
  56. package/src/ir-to-client-js/reactivity.ts +4 -2
  57. package/src/ir-to-client-js/types.ts +16 -0
  58. package/src/ir-to-client-js/utils.ts +15 -0
  59. package/src/jsx-to-ir.ts +117 -1
  60. package/src/module-exports.ts +137 -0
  61. package/src/scope/binding-scope.ts +1 -1
  62. package/src/types.ts +118 -0
@@ -757,7 +757,7 @@ hydrate('Example', { init: initExample, template: (_p) => \`<div><button bf="s0"
757
757
  export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
758
758
  `;
759
759
 
760
- exports[`docs/core/rendering/jsx-compatibility.md doc-examples L142 — ✅ Nested \`.filter()\` / \`.map()\` now compiles everywhere 1`] = `
760
+ exports[`docs/core/rendering/jsx-compatibility.md doc-examples L143 — ✅ Nested \`.filter()\` / \`.map()\` now compiles everywhere 1`] = `
761
761
  "import { createComponent, createEffect, createSignal, escapeText, escapeTextOrNode, hydrate, lazySlots } from '@barefootjs/client/runtime'
762
762
 
763
763
  export function initTodoItem(__scope, _p = {}) {
@@ -815,7 +815,7 @@ hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s1"><!--bf:
815
815
  export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
816
816
  `;
817
817
 
818
- exports[`docs/core/rendering/jsx-compatibility.md doc-examples L149 — ❌ BF101 on Go/Mojo; works on Hono 1`] = `
818
+ exports[`docs/core/rendering/jsx-compatibility.md doc-examples L150 — ❌ BF101 on Go/Mojo; works on Hono 1`] = `
819
819
  "import { createComponent, createEffect, createSignal, escapeText, escapeTextOrNode, hydrate, lazySlots } from '@barefootjs/client/runtime'
820
820
 
821
821
  export function initTodoItem(__scope, _p = {}) {
@@ -873,7 +873,7 @@ hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s1"><!--bf:
873
873
  export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
874
874
  `;
875
875
 
876
- exports[`docs/core/rendering/jsx-compatibility.md doc-examples L152 — ✅ Add /* @client */ to evaluate on the client 1`] = `
876
+ exports[`docs/core/rendering/jsx-compatibility.md doc-examples L153 — ✅ Add /* @client */ to evaluate on the client 1`] = `
877
877
  "import { createComponent, createEffect, createSignal, escapeText, escapeTextOrNode, hydrate, lazySlots } from '@barefootjs/client/runtime'
878
878
 
879
879
  export function initTodoItem(__scope, _p = {}) {
@@ -927,11 +927,11 @@ export function initExample(__scope, _p = {}) {
927
927
 
928
928
  }
929
929
 
930
- hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s1"><!--bf:s0--><!--/--></div>\` })
930
+ hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s1"></div>\` })
931
931
  export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
932
932
  `;
933
933
 
934
- exports[`docs/core/rendering/jsx-compatibility.md doc-examples L159 — ❌ BF101 on Go/Mojo; works on Hono 1`] = `
934
+ exports[`docs/core/rendering/jsx-compatibility.md doc-examples L160 — ❌ BF101 on Go/Mojo; works on Hono 1`] = `
935
935
  "import { createComponent, createEffect, createSignal, escapeText, escapeTextOrNode, hydrate, lazySlots } from '@barefootjs/client/runtime'
936
936
 
937
937
  export function initTodoItem(__scope, _p = {}) {
@@ -989,7 +989,7 @@ hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s1"><!--bf:
989
989
  export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
990
990
  `;
991
991
 
992
- exports[`docs/core/rendering/jsx-compatibility.md doc-examples L162 — ✅ Use /* @client */ 1`] = `
992
+ exports[`docs/core/rendering/jsx-compatibility.md doc-examples L163 — ✅ Use /* @client */ 1`] = `
993
993
  "import { createComponent, createEffect, createSignal, escapeText, escapeTextOrNode, hydrate, lazySlots } from '@barefootjs/client/runtime'
994
994
 
995
995
  export function initTodoItem(__scope, _p = {}) {
@@ -1043,11 +1043,11 @@ export function initExample(__scope, _p = {}) {
1043
1043
 
1044
1044
  }
1045
1045
 
1046
- hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s1"><!--bf:s0--><!--/--></div>\` })
1046
+ hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s1"></div>\` })
1047
1047
  export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
1048
1048
  `;
1049
1049
 
1050
- exports[`docs/core/rendering/jsx-compatibility.md doc-examples L179 — ❌ BF101 on Go/Mojo; works on Hono 1`] = `
1050
+ exports[`docs/core/rendering/jsx-compatibility.md doc-examples L180 — ❌ BF101 on Go/Mojo; works on Hono 1`] = `
1051
1051
  "import { createComponent, createEffect, createSignal, escapeText, escapeTextOrNode, hydrate, lazySlots } from '@barefootjs/client/runtime'
1052
1052
 
1053
1053
  export function initTodoItem(__scope, _p = {}) {
@@ -1105,7 +1105,7 @@ hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s1"><!--bf:
1105
1105
  export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
1106
1106
  `;
1107
1107
 
1108
- exports[`docs/core/rendering/jsx-compatibility.md doc-examples L182 — ✅ Use arrow functions for adapter portability 1`] = `
1108
+ exports[`docs/core/rendering/jsx-compatibility.md doc-examples L183 — ✅ Use arrow functions for adapter portability 1`] = `
1109
1109
  "import { createComponent, createEffect, createSignal, escapeText, escapeTextOrNode, hydrate, lazySlots } from '@barefootjs/client/runtime'
1110
1110
 
1111
1111
  export function initTodoItem(__scope, _p = {}) {
@@ -1163,7 +1163,107 @@ hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s1"><!--bf:
1163
1163
  export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
1164
1164
  `;
1165
1165
 
1166
- exports[`docs/core/rendering/jsx-compatibility.md doc-examples L195 Value-producing block bodies normalize (let-inline) and lower everywhere 1`] = `
1166
+ exports[`docs/core/rendering/jsx-compatibility.md doc-examples L192 BF101 on Go/Mojo/Xslate/Twig/ERB/Blade/Jinja/MiniJinja; works on Hono 1`] = `
1167
+ "import { $, createComponent, createEffect, escapeAttr, escapeText, hydrate, lazySlots } from '@barefootjs/client/runtime'
1168
+
1169
+
1170
+ export function initReactionBar(__scope, _p = {}) {
1171
+ if (!__scope) return
1172
+ const __scopeId = __scope.getAttribute('bf-s')
1173
+
1174
+ const entries = Object.entries(_p.reactions).filter(([, users]) => users.length > 0)
1175
+
1176
+ const [_s2] = $(__scope, 's2')
1177
+
1178
+ // Reactive texts / CSR materialize in static array children
1179
+ if (_s2) {
1180
+ entries.forEach(([emoji, users], __idx) => {
1181
+ let __iterEl = _s2.children[__idx]
1182
+ if (!__iterEl) {
1183
+ const __tpl = document.createElement('template')
1184
+ __tpl.innerHTML = \`<span data-key="\${escapeAttr(emoji)}"><!--bf:s0-->\${escapeText(emoji)}<!--/-->: <!--bf:s1-->\${escapeText(String(users.length))}<!--/--></span>\`
1185
+ const __cloned = __tpl.content.firstElementChild
1186
+ if (__cloned) {
1187
+ const __anchor = _s2.children[__idx] ?? null
1188
+ _s2.insertBefore(__cloned, __anchor)
1189
+ __iterEl = __cloned
1190
+ }
1191
+ }
1192
+ if (__iterEl) {
1193
+ const __bfw_s0 = lazySlots(__iterEl, [{ id: 's0', kind: 'text', path: [] }, { id: 's1', kind: 'text', path: [] }])
1194
+ createEffect(() => { __bfw_s0('s0', String(emoji)) })
1195
+ createEffect(() => { __bfw_s0('s1', String(String(users.length))) })
1196
+ }
1197
+ })
1198
+ }
1199
+
1200
+ }
1201
+
1202
+ hydrate('ReactionBar__b3c36eee', { init: initReactionBar, template: (_p) => \`<div bf="s2"><!--bf-loop:l0-->\${[].map(([emoji, users]) => \`<span data-key="\${escapeAttr(emoji)}"><!--bf:s0-->\${escapeText(emoji)}<!--/-->: <!--bf:s1-->\${escapeText(String(users.length))}<!--/--></span>\`).join('')}<!--bf-/loop:l0--></div>\`, name: 'ReactionBar' })
1203
+ export function ReactionBar(_p, __bfKey) { return createComponent('ReactionBar__b3c36eee', _p, __bfKey) }"
1204
+ `;
1205
+
1206
+ exports[`docs/core/rendering/jsx-compatibility.md doc-examples L201 — ✅ Best: precompute the array in the parent/route handler and pass it as a prop 1`] = `
1207
+ "import { $, createComponent, escapeAttr, escapeText, hydrate, lazySlots, mapArrayLazy, textOrNode } from '@barefootjs/client/runtime'
1208
+
1209
+
1210
+ export function initReactionBarByProp(__scope, _p = {}) {
1211
+ if (!__scope) return
1212
+ const __scopeId = __scope.getAttribute('bf-s')
1213
+
1214
+ const entries = _p.entries ?? []
1215
+
1216
+ const [_s2] = $(__scope, 's2')
1217
+
1218
+ const __tpl_l0 = document.createElement('template')
1219
+ __tpl_l0.innerHTML = \`<span data-key=""><!--bf:s0--><!--/-->: <!--bf:s1--><!--/--></span>\`
1220
+ const __lzp_l0 = [[0], [3]]
1221
+ const __lzs_l0 = [{ id: 's0', kind: 'text', path: [] }, { id: 's1', kind: 'text', path: [] }]
1222
+ const __lzsc_l0 = [{ id: 's0', kind: 'text', path: __lzp_l0[0] }, { id: 's1', kind: 'text', path: __lzp_l0[1] }]
1223
+ mapArrayLazy(() => entries, _s2, ([emoji, users]) => String(emoji), {
1224
+ createRow: (__e, __idx) => {
1225
+ const __bfItem = () => __e.item
1226
+ const __el = __tpl_l0.content.firstElementChild.cloneNode(true)
1227
+ const __r = __e.refs = [lazySlots(__el, __lzsc_l0)]
1228
+ const __l = __e.last = []
1229
+ { const __x = __bfItem()[0]
1230
+ __r[0]('s0', textOrNode(__x))
1231
+ __l[0] = __x }
1232
+ { const __x = String(__bfItem()[1].length)
1233
+ __r[0]('s1', textOrNode(__x))
1234
+ __l[1] = __x }
1235
+ return __el
1236
+ },
1237
+ applyItem: (__e) => {
1238
+ const __bfItem = () => __e.item
1239
+ const __r = __e.refs ?? (__e.refs = [])
1240
+ const __l = __e.last ?? (__e.last = [])
1241
+ const __d = __r[0] ?? (__r[0] = lazySlots(__e.primaryEl, __lzs_l0))
1242
+ { const __x = __bfItem()[0]
1243
+ if (!(0 in __l) || !Object.is(__l[0], __x)) __d('s0', textOrNode(__x))
1244
+ __l[0] = __x }
1245
+ { const __x = String(__bfItem()[1].length)
1246
+ if (!(1 in __l) || !Object.is(__l[1], __x)) __d('s1', textOrNode(__x))
1247
+ __l[1] = __x }
1248
+ },
1249
+ }, 'l0')
1250
+
1251
+ }
1252
+
1253
+ hydrate('ReactionBarByProp__b3c36eee', { init: initReactionBarByProp, template: (_p) => \`<div bf="s2"><!--bf-loop:l0-->\${_p.entries.map(([emoji, users]) => \`<span data-key="\${escapeAttr(emoji)}"><!--bf:s0-->\${escapeText(emoji)}<!--/-->: <!--bf:s1-->\${escapeText(String(users.length))}<!--/--></span>\`).join('')}<!--bf-/loop:l0--></div>\`, name: 'ReactionBarByProp' })
1254
+ export function ReactionBarByProp(_p, __bfKey) { return createComponent('ReactionBarByProp__b3c36eee', _p, __bfKey) }"
1255
+ `;
1256
+
1257
+ exports[`docs/core/rendering/jsx-compatibility.md doc-examples L209 — ✅ Or defer to the client with /* @client */ 1`] = `
1258
+ "import { createComponent, hydrate } from '@barefootjs/client/runtime'
1259
+
1260
+ function initStatementExample() {}
1261
+
1262
+ hydrate('StatementExample', { init: initStatementExample, template: (_p) => \`<div></div>\` })
1263
+ export function StatementExample(_p, __bfKey) { return createComponent('StatementExample', _p, __bfKey) }"
1264
+ `;
1265
+
1266
+ exports[`docs/core/rendering/jsx-compatibility.md doc-examples L229 — ✅ Value-producing block bodies normalize (let-inline) and lower everywhere 1`] = `
1167
1267
  "import { $, createComponent, createEffect, createSignal, escapeText, escapeTextOrNode, hydrate, initChild, lazySlots, mapArray, renderChild } from '@barefootjs/client/runtime'
1168
1268
 
1169
1269
  export function initTodoItem(__scope, _p = {}) {
@@ -1222,7 +1322,7 @@ hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s1"><!--bf-
1222
1322
  export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
1223
1323
  `;
1224
1324
 
1225
- exports[`docs/core/rendering/jsx-compatibility.md doc-examples L205 — ❌ BF021 on Go/Mojo — a JS-runtime target (Hono, CSR) runs the comparator 1`] = `
1325
+ exports[`docs/core/rendering/jsx-compatibility.md doc-examples L239 — ❌ BF021 on Go/Mojo — a JS-runtime target (Hono, CSR) runs the comparator 1`] = `
1226
1326
  "import { $, createComponent, createEffect, createSignal, escapeText, escapeTextOrNode, hydrate, initChild, lazySlots, mapArray, renderChild } from '@barefootjs/client/runtime'
1227
1327
 
1228
1328
  export function initTodoItem(__scope, _p = {}) {
@@ -1281,7 +1381,7 @@ hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s1"><!--bf-
1281
1381
  export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
1282
1382
  `;
1283
1383
 
1284
- exports[`docs/core/rendering/jsx-compatibility.md doc-examples L210 — ✅ Use /* @client */ 1`] = `
1384
+ exports[`docs/core/rendering/jsx-compatibility.md doc-examples L244 — ✅ Use /* @client */ 1`] = `
1285
1385
  "import { $, createComponent, createEffect, createSignal, escapeText, escapeTextOrNode, hydrate, initChild, lazySlots, mapArray, renderChild } from '@barefootjs/client/runtime'
1286
1386
 
1287
1387
  export function initTodoItem(__scope, _p = {}) {
@@ -1340,7 +1440,7 @@ hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s1"><!--bf-
1340
1440
  export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
1341
1441
  `;
1342
1442
 
1343
- exports[`docs/core/rendering/jsx-compatibility.md doc-examples L222 — ❌ BF021 on Go/Mojo — \`byPrice\` is imported, not declared in this file 1`] = `
1443
+ exports[`docs/core/rendering/jsx-compatibility.md doc-examples L256 — ❌ BF021 on Go/Mojo — \`byPrice\` is imported, not declared in this file 1`] = `
1344
1444
  "import { $, createComponent, createEffect, escapeAttr, escapeText, hydrate, lazySlots, mapArray } from '@barefootjs/client/runtime'
1345
1445
  import { byPrice } from './comparators'
1346
1446
 
@@ -1368,7 +1468,7 @@ hydrate('SortedList__b3c36eee', { init: initSortedList, template: (_p) => \`<ul
1368
1468
  export function SortedList(_p, __bfKey) { return createComponent('SortedList__b3c36eee', _p, __bfKey) }"
1369
1469
  `;
1370
1470
 
1371
- exports[`docs/core/rendering/jsx-compatibility.md doc-examples L228 — ✅ Use /* @client */, or inline / re-declare the comparator locally 1`] = `
1471
+ exports[`docs/core/rendering/jsx-compatibility.md doc-examples L262 — ✅ Use /* @client */, or inline / re-declare the comparator locally 1`] = `
1372
1472
  "import { $, createComponent, createEffect, createSignal, escapeText, escapeTextOrNode, hydrate, initChild, lazySlots, mapArray, renderChild } from '@barefootjs/client/runtime'
1373
1473
 
1374
1474
  export function initTodoItem(__scope, _p = {}) {
@@ -1481,7 +1581,7 @@ export function initExample(__scope, _p = {}) {
1481
1581
 
1482
1582
  }
1483
1583
 
1484
- hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s1"><!--bf:s0--><!--/--></div>\` })
1584
+ hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s1"></div>\` })
1485
1585
  export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
1486
1586
  `;
1487
1587
 
@@ -1539,7 +1639,7 @@ export function initExample(__scope, _p = {}) {
1539
1639
 
1540
1640
  }
1541
1641
 
1542
- hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s1"><!--bf:s0--><!--/--></div>\` })
1642
+ hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s1"></div>\` })
1543
1643
  export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
1544
1644
  `;
1545
1645
 
@@ -1597,7 +1697,7 @@ export function initExample(__scope, _p = {}) {
1597
1697
 
1598
1698
  }
1599
1699
 
1600
- hydrate('Example', { init: initExample, template: (_p) => \`<div><strong bf="s1"><!--bf:s0--><!--/--></strong></div>\` })
1700
+ hydrate('Example', { init: initExample, template: (_p) => \`<div><strong bf="s1"></strong></div>\` })
1601
1701
  export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
1602
1702
  `;
1603
1703
 
@@ -12,7 +12,9 @@
12
12
  * pattern covers both.
13
13
  * Modeled in spirit on `map-body-no-silent-divergence.test.ts`'s known-hole
14
14
  * ledger: a known-inventory of the current reality that may only shrink as
15
- * Stages 1-4 migrate call sites onto `BindingScope`, never grow.
15
+ * Stages 1-4 migrate call sites onto `BindingScope`, never grow. Stage 4
16
+ * drove this to its FLOOR — every remaining entry is a permanent, justified
17
+ * exception (see the `ALLOWLIST` doc comment below), not leftover debt.
16
18
  *
17
19
  * Scans every `.ts` file under `packages/jsx/src/` and each `packages/adapter-X/src/`
18
20
  * (excluding `__tests__` directories, `*.test.ts` files, and
@@ -58,44 +60,167 @@ const PATTERNS = [
58
60
  type Pattern = (typeof PATTERNS)[number]
59
61
 
60
62
  /**
61
- * Exact per-file, per-pattern occurrence counts as of #2482 Stage 0. Only
62
- * ever shrinks — see header comment. A missing file or missing pattern key
63
- * means an expected count of 0.
63
+ * Exact per-file, per-pattern occurrence counts, updated through #2482
64
+ * Stage 4 (the FLOOR — see that stage's PR for the file-by-file
65
+ * MIGRATE-or-FLOOR review). Only ever shrinks — see header comment. A
66
+ * missing file or missing pattern key means an expected count of 0.
67
+ *
68
+ * ============================== FLOOR INVARIANT ==============================
69
+ * Every entry below is a PERMANENT, JUSTIFIED exception, not leftover debt —
70
+ * each one is annotated with why it can't (or shouldn't) thread a live
71
+ * `BindingScope` instead. Two shapes recur across the justifications:
72
+ *
73
+ * 1. NO LIVE SCOPE TO CONSULT: a prepass that runs once at `generate()` /
74
+ * constructor-generation time, OUTSIDE the render-time (or
75
+ * render-shaped) tree walk `BindingScope` is threaded through — there is
76
+ * no position to ask "what's bound HERE" about, so these fall back to a
77
+ * coarser, deliberately over-inclusive whole-component or whole-file
78
+ * scan (safe because over-exclusion only ever degrades to the
79
+ * already-accepted pre-#2482 residual, never to silently wrong output).
80
+ * 2. UNRELATED DOMAIN: a `localConstants.find(` matching the ledger's
81
+ * textual pattern by coincidence — const/prop resolution for SSR
82
+ * dead-code elimination, `Record[key]` lookup, cross-IR CSS class
83
+ * resolution, component-scope const-chain inlining — none of which ask
84
+ * "is this name shadowed by a loop callback param" at all, so
85
+ * `BindingScope` (a loop-row/callback binding stack) has no bearing on
86
+ * them.
87
+ * 3. ACCESSOR/REWRITE PAYLOAD, NOT AN EXISTENCE QUERY: `BindingScope` only
88
+ * carries binding EXISTENCE/kind/depth — never per-binding rendering
89
+ * payload (a Go template accessor string, an ordered client-JS
90
+ * accessor-rewrite spec). The Go adapter's `loopBindingStack` and the
91
+ * client-JS emitter's `loopParams: ReadonlyArray<string | LoopParamSpec>`
92
+ * (`ir-to-client-js/utils.ts`'s `wrapExprWithLoopParams` and its
93
+ * `collect-elements.ts`/`html-template.ts`/`build-event-delegation.ts`
94
+ * callers) are this shape — a `BindingScope`-adjacent but structurally
95
+ * distinct concern.
96
+ *
97
+ * New code must use `BindingScope` (`packages/jsx/src/scope/binding-scope.ts`)
98
+ * for any "is this name bound by an enclosing loop callback" question. Per
99
+ * the mechanics already documented below: entries may only shrink (or be
100
+ * removed outright when a file reaches 0), never grow — a grown or added
101
+ * entry is a regression to flag in review, not to allowlist away.
102
+ * ===============================================================================
64
103
  */
65
104
  const ALLOWLIST: Record<string, Partial<Record<Pattern, number>>> = {
66
- 'packages/adapter-blade/src/adapter/blade-adapter.ts': { staticLoopSourceBoundNames: 8, loopBoundNames: 12 },
67
- 'packages/adapter-erb/src/adapter/erb-adapter.ts': { 'localConstants.find(': 1, loopBoundNames: 20 },
68
- 'packages/adapter-erb/src/adapter/expr/emitters.ts': { loopBoundNames: 1 },
105
+ // FLOOR (shape 1, already guarded): `expandDynamicPropValue`'s
106
+ // `this.scope`-mirroring shadow guard (`erb-adapter.ts`'s own
107
+ // `!this.scope.isBound(trimmed)` check, #2489) precedes this `.find(` —
108
+ // the lookup itself is the legitimate const-resolution step once the
109
+ // shadow question is already answered by `scope`.
110
+ 'packages/adapter-erb/src/adapter/erb-adapter.ts': { 'localConstants.find(': 1 },
111
+ // FLOOR (shape 2/3 boundary): `inlineLocalHelperCall` resolves a call's
112
+ // CALLEE name against module/component consts to inline a local
113
+ // arrow-helper's body (`sortClass(k)` → its substituted expression) — a
114
+ // "does a helper by this name exist" lookup, not itself a shadow guard.
115
+ // Flagged for the human maintainer (see Stage 4 report): unlike the
116
+ // sibling `litConst` fast path directly above its call site in
117
+ // `go-template-adapter.ts` (which DOES guard with `isLoopShadowedName`),
118
+ // this call site has no such guard — a `.map((sortClass) => ...)`
119
+ // shadowing a same-named module helper is a narrow, unverified gap left
120
+ // for a follow-up rather than fixed speculatively here.
69
121
  'packages/adapter-go-template/src/adapter/expr/helper-inline.ts': { 'localConstants.find(': 1 },
70
122
  'packages/adapter-go-template/src/adapter/go-template-adapter.ts': {
123
+ // FLOOR: of the 5 `.find(` sites — `resolveDynamicPropValue` (child-prop
124
+ // passthrough, called only from `generateNewPropsFunction`'s
125
+ // `emitStaticChildInstances`), `computeDerivedConstFields` and
126
+ // `isStringExpr` (same `generateNewPropsFunction` constructor-context
127
+ // bucket), and `resolveModuleNumericConst` (guarded in place by
128
+ // `isCurrentLoopItem`/`isOuterLoopParam`, both already `this.scope.lookup`-
129
+ // backed) — 3 are shape-1 (no live scope in the ctor-generation prepass)
130
+ // and 1 is already scope-guarded. The 5th, `renderLoop`'s loop-array
131
+ // const lookup, gained a `!this.scope.isBound(arrayName)` guard in
132
+ // Stage 4 (a real gap: an enclosing loop's own item param could shadow
133
+ // a same-named module const and misfire a BF101) — MIGRATED in place,
134
+ // the `.find(` call itself stays as the legitimate lookup once shadow
135
+ // is ruled out.
71
136
  'localConstants.find(': 5,
72
- staticLoopSourceBoundNames: 3,
73
- loopParamStack: 35,
137
+ // #2482 Stage 3: `loopParamStack` eliminated entirely (0, down from 35)
138
+ // — replaced by the threaded `this.scope: BindingScope`. The remaining
139
+ // 2 `staticLoopSourceBoundNames` uses (down from 3) are the
140
+ // `getBakedStaticChildLoop` shadow guard shared with two call sites
141
+ // OUTSIDE the live `renderLoop` tree walk (no live `scope` to consult
142
+ // there — shape 1) — a genuinely-legitimate surviving use, confirmed
143
+ // FLOOR in Stage 4 (`primeCompileState`'s own comment documents the
144
+ // three-call-site agreement requirement).
145
+ staticLoopSourceBoundNames: 2,
74
146
  },
75
- 'packages/adapter-go-template/src/adapter/lib/compile-state.ts': { staticLoopSourceBoundNames: 1, loopParamStack: 1 },
147
+ // FLOOR (shape 1 for the field; the doc-only `loopParamStack` mention this
148
+ // file's top-level comment used to carry was reworded in Stage 4 — that
149
+ // stack no longer exists anywhere, so the ledger's last trace of it is
150
+ // gone too).
151
+ 'packages/adapter-go-template/src/adapter/lib/compile-state.ts': { staticLoopSourceBoundNames: 1 },
152
+ // FLOOR (shape 1): `lowerCtorExpr`/`lowerCtorStringArray` lower a derived-
153
+ // state memo's computation into Go CODE evaluated in the `NewXxxProps`
154
+ // constructor — the same no-live-scope bucket as `go-template-adapter.ts`'s
155
+ // ctor-context `.find(` calls above.
76
156
  'packages/adapter-go-template/src/adapter/memo/ctor-lowering.ts': { 'localConstants.find(': 3 },
157
+ // FLOOR (shape 1): `packageModuleConst` — same `NewXxxProps` ctor-context
158
+ // bucket as `ctor-lowering.ts`.
77
159
  'packages/adapter-go-template/src/adapter/memo/memo-value.ts': { 'localConstants.find(': 1 },
78
- 'packages/adapter-jinja/src/adapter/jinja-adapter.ts': { staticLoopSourceBoundNames: 8, loopBoundNames: 12 },
160
+ // FLOOR (shape 1, already guarded): same `this.scope.isBound(trimmed)`
161
+ // shadow guard as `erb-adapter.ts` (#2221 — `resolveLiteralConst` /
162
+ // `resolveStaticRecordLiteral`'s established pattern).
79
163
  'packages/adapter-mojolicious/src/adapter/mojo-adapter.ts': {
80
164
  'localConstants.find(': 1,
81
- staticLoopSourceBoundNames: 1,
82
- loopBoundNames: 23,
83
165
  },
84
- 'packages/adapter-rust/src/adapter/minijinja-adapter.ts': { staticLoopSourceBoundNames: 8, loopBoundNames: 12 },
85
- 'packages/adapter-twig/src/adapter/twig-adapter.ts': { staticLoopSourceBoundNames: 8, loopBoundNames: 12 },
86
- 'packages/adapter-xslate/src/adapter/xslate-adapter.ts': { staticLoopSourceBoundNames: 8, loopBoundNames: 12 },
166
+ // FLOOR (shape 2): `generateSignalInitializers`'s reachability analysis
167
+ // for SSR no-op initializer dead-code elimination module/component-level
168
+ // declaration reachability, unrelated to loop-row shadowing entirely.
87
169
  'packages/jsx/src/adapters/jsx-adapter.ts': { 'localConstants.find(': 1 },
170
+ // FLOOR (shape 2): resolves a `Record<T,string>[key]`-shaped indexed
171
+ // lookup's IDENT operand to a module-scope object-literal const for
172
+ // `renderToTest`'s union-semantics resolution — a component-level static
173
+ // analysis pass, not a loop-row shadow question.
88
174
  'packages/jsx/src/augment-inherited-props.ts': { 'localConstants.find(': 1 },
175
+ // FLOOR (shape 2): cross-IR CSS class-const resolution for the UnoCSS
176
+ // layer prefixer's transitive-reference walk — operates across whole
177
+ // components' `localConstants` lists, no loop-row scope involved.
89
178
  'packages/jsx/src/css-layer-prefixer.ts': { 'localConstants.find(': 1 },
90
- 'packages/jsx/src/debug.ts': { loopParams: 18 },
91
- 'packages/jsx/src/free-refs.ts': { 'localConstants.find(': 1, loopParams: 4 },
179
+ 'packages/jsx/src/free-refs.ts': {
180
+ // FLOOR (shape 2): `resolveConstantInitializerRefs`'s transitive-taint
181
+ // expansion — resolves a local constant's OWN initializer by name to
182
+ // recurse into its free refs, not a loop-row shadow check.
183
+ 'localConstants.find(': 1,
184
+ // FLOOR: the `BindingEnvironment.loopParams` field itself was RENAMED
185
+ // (Stage 4) to `loopValueBoundNames` (it carries `scope.valueBoundNames()`
186
+ // — the field name now says so). This single remaining occurrence is the
187
+ // rename's own historical-name mention in `loopValueBoundNames`'s
188
+ // docstring, matching the established convention elsewhere (e.g.
189
+ // `binding-scope.ts`'s own header keeps six `ctx.loopParams` mentions as
190
+ // migration history).
191
+ loopParams: 1,
192
+ },
193
+ // FLOOR (shape 3): `irToPlaceholderTemplate`'s loop-param accessor-rewrite
194
+ // spec forwarding — see the canonical docstring on
195
+ // `wrapExprWithLoopParams` in `ir-to-client-js/utils.ts`.
92
196
  'packages/jsx/src/ir-to-client-js/collect-elements.ts': { loopParams: 9 },
197
+ // FLOOR (shape 2): `computeCsrInlinability`'s fixed-point constant-chain
198
+ // inlining loop over `ctx.localConstants` — component-scope const
199
+ // resolution, not loop-row scope.
93
200
  'packages/jsx/src/ir-to-client-js/compute-inlinability.ts': { 'localConstants.find(': 1 },
201
+ // FLOOR (shape 3): event-delegation plan building deliberately does NOT
202
+ // pass a loop-param accessor-rewrite spec (see the file's own comments) —
203
+ // the remaining mentions are all in that reasoning, not device usage.
94
204
  'packages/jsx/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts': { loopParams: 4 },
95
- 'packages/jsx/src/ir-to-client-js/html-template.ts': { loopParams: 16 },
205
+ // FLOOR (shape 3): `irToHtmlTemplate`/`irToPlaceholderTemplate`'s
206
+ // loop-param accessor-rewrite spec — see `ir-to-client-js/utils.ts`'s
207
+ // `wrapExprWithLoopParams` docstring (the canonical explanation, pointed
208
+ // to from this file's own function signature since Stage 4).
209
+ 'packages/jsx/src/ir-to-client-js/html-template.ts': { loopParams: 17 },
210
+ // FLOOR (shape 1, already guarded): both `expandDynamicPropValue` and
211
+ // `expandConstantForReactivity` precede their `.find(` with
212
+ // `scope?.isBound(trimmedValue)` — see this file's own header comment
213
+ // (added Stage 1b) for the full SHADOW GUARD reasoning.
96
214
  'packages/jsx/src/ir-to-client-js/prop-handling.ts': { 'localConstants.find(': 2 },
97
- 'packages/jsx/src/ir-to-client-js/utils.ts': { loopParams: 3 },
98
- 'packages/jsx/src/jsx-to-ir.ts': { loopParams: 1 },
215
+ // FLOOR (shape 3): `wrapExprWithLoopParams` / `LoopParamSpec` — the
216
+ // canonical definition of the accessor-rewrite payload every other
217
+ // `loopParams`-named parameter in `ir-to-client-js/` forwards. See its
218
+ // docstring (added Stage 4) for the full shape-3 reasoning.
219
+ 'packages/jsx/src/ir-to-client-js/utils.ts': { loopParams: 4 },
220
+ // `jsx-to-ir.ts` — MIGRATED to 0 in Stage 4: `makeBindingEnv`'s
221
+ // `loopParams: boundNames` field now reads `loopValueBoundNames:
222
+ // boundNames`, matching `free-refs.ts`'s renamed field. No entry needed
223
+ // (a missing key means an expected count of 0).
99
224
  }
100
225
 
101
226
  const SCOPE_MODULE_DIR = join(REPO_ROOT, 'packages', 'jsx', 'src', 'scope')
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Regression test for #2573 (xyflow TS2304 family): a component function's
3
+ * own generic type parameters (`function Flow<NodeType, EdgeType>(...)`)
4
+ * were dropped from the emitted `.tsx` SSR template — the function
5
+ * signature came out as `function Flow(...)` even though its props type
6
+ * annotation (and often its body) kept referencing `NodeType`/`EdgeType`
7
+ * verbatim (`props: FlowComponentProps<NodeType, EdgeType>`,
8
+ * `createFlowStore<NodeType, EdgeType>(props)`). TypeScript then reported
9
+ * "Cannot find name 'NodeType'" (TS2304) at every such reference — the
10
+ * declaration that would have brought the name into scope was never
11
+ * emitted. Runtime output (client JS) was always correct; this is a
12
+ * type-level emission defect in the SSR template only.
13
+ *
14
+ * `IRMetadata.typeParameters` now carries the source's type parameter list
15
+ * verbatim (`node.getText()` per parameter, mirroring `ConstantInfo.
16
+ * typeAnnotation`'s #2589 precedent), and `HonoAdapter`/`TestAdapter` splice
17
+ * it between the function name and the parameter list.
18
+ */
19
+
20
+ import { describe, test, expect } from 'bun:test'
21
+ import { compileJSX } from '../compiler'
22
+ import { HonoAdapter } from '../../../../packages/adapter-hono/src/adapter/hono-adapter'
23
+
24
+ describe('component type parameter preservation in emitted templates (#2573)', () => {
25
+ test('a generic function component keeps its type parameters on the emitted signature', () => {
26
+ const honoAdapter = new HonoAdapter()
27
+ const source = `
28
+ interface NodeBase { id: string }
29
+ interface EdgeBase { id: string }
30
+ interface FlowProps<NodeType extends NodeBase, EdgeType extends EdgeBase> {
31
+ nodes: NodeType[]
32
+ edges: EdgeType[]
33
+ }
34
+
35
+ export function Flow<NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase>(
36
+ props: FlowProps<NodeType, EdgeType>,
37
+ ) {
38
+ const count = props.nodes.length + props.edges.length
39
+ return <div>{count}</div>
40
+ }
41
+ `
42
+
43
+ const result = compileJSX(source, 'Flow.tsx', { adapter: honoAdapter })
44
+ expect(result.errors).toHaveLength(0)
45
+
46
+ const template = result.files.find((f) => f.type === 'markedTemplate')!
47
+ expect(template).toBeDefined()
48
+ expect(template.content).toContain(
49
+ 'function Flow<NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase>(',
50
+ )
51
+ })
52
+
53
+ test('a non-generic function component gains no type parameter clause', () => {
54
+ const honoAdapter = new HonoAdapter()
55
+ const source = `
56
+ interface CardProps { title: string }
57
+ export function Card(props: CardProps) {
58
+ return <div>{props.title}</div>
59
+ }
60
+ `
61
+
62
+ const result = compileJSX(source, 'Card.tsx', { adapter: honoAdapter })
63
+ expect(result.errors).toHaveLength(0)
64
+
65
+ const template = result.files.find((f) => f.type === 'markedTemplate')!
66
+ expect(template).toBeDefined()
67
+ expect(template.content).toContain('function Card(')
68
+ expect(template.content).not.toMatch(/function Card</)
69
+ })
70
+ })
@@ -69,7 +69,16 @@ describe('CSR materialize template vs .map() preamble-local shadowing a module c
69
69
 
70
70
  // The row-local `label` (the preamble's OWN computed value) must
71
71
  // drive the branch — never the outer module const's literal.
72
- expect(tpl).toContain('label ? `<span>yes</span>`')
72
+ //
73
+ // The branch also carries a `bf-c="s0"` slot marker on both arms
74
+ // (#2596 follow-up): `label`'s initializer reads the real signal `on()`,
75
+ // so `markPreambleConditionalReactivity` now grants this conditional a
76
+ // slot id and the `reactive` flag it lacked before — the SAME
77
+ // observable-failure-mode class this file's docstring describes
78
+ // ("every row's branch condition evaluated the constant's fixed
79
+ // truthiness"), just for the signal dimension instead of the shadowing
80
+ // one. Nothing else about this fixture's shape changed.
81
+ expect(tpl).toContain('label ? `<span bf-c="s0">yes</span>` : `<span bf-c="s0">no</span>`')
73
82
  expect(tpl).not.toContain("('MODULE_CONST')")
74
83
  expect(tpl).not.toContain('MODULE_CONST')
75
84
  })
@@ -460,6 +460,7 @@ const ERROR_CODES_DOC_TOO_MINIMAL: Record<string, string> = {
460
460
  BF021: 'BF021 has multiple ❌ snippets in placeholder form (`.map(...)`) — covered concretely by `jsx-compatibility.md` tests; the doc-form snippets here remain placeholder',
461
461
  BF043: 'BF043 fires only for STATEFUL components (signals/memos/effects). The doc snippet `function Child({ count }: Props)` has no reactivity, so the check is correctly silent — doc snippet needs enrichment to be testable as-is',
462
462
  BF044: 'BF044 fires only when a real signal getter is bound. The doc snippet `<Child count={count} />` references an undeclared `count`, so the check is silent — doc snippet needs enrichment',
463
+ BF101: 'BF101 is raised by the non-JS template adapters\' own per-adapter checks (Go/Mojo/Xslate/… `renderLoop` / callback lowering), not by the shared `isSupported` gate every adapter shares — `TestAdapter` here is JS-runtime-style like Hono and executes both doc snippets verbatim, so the check is correctly silent. Covered concretely (compiled against the real 8 template adapters, with verbatim diagnostic text) by `jsx-compatibility.md`\'s doc-examples coverage and the `filter-nested-callback-predicate` / `filter-nested-find-predicate` / `static-array-from-props` adapter-conformance fixtures.',
463
464
  }
464
465
 
465
466
  // BFxxx codes that `errors.ts` defines but no production code in
@@ -110,7 +110,7 @@ describe('resolveFreeRefs — kind resolution', () => {
110
110
  const env = {
111
111
  ...emptyEnv(),
112
112
  localConstants: [mkConst('item')],
113
- loopParams: new Set(['item']),
113
+ loopValueBoundNames: new Set(['item']),
114
114
  }
115
115
  const refs = resolveFreeRefs(parseExpression('item'), env)
116
116
  expect(refs.find(r => r.name === 'item')?.kind).toBe('render-item')