@barefootjs/jsx 0.26.4 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/compiler.d.ts.map +1 -1
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +321 -104
  5. package/dist/ir-to-client-js/client-only-elision.d.ts +103 -0
  6. package/dist/ir-to-client-js/client-only-elision.d.ts.map +1 -0
  7. package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
  8. package/dist/ir-to-client-js/control-flow/shared.d.ts +1 -1
  9. package/dist/ir-to-client-js/control-flow/stringify/claim-plan.d.ts +54 -0
  10. package/dist/ir-to-client-js/control-flow/stringify/claim-plan.d.ts.map +1 -0
  11. package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts.map +1 -1
  12. package/dist/ir-to-client-js/control-flow/stringify/insert.d.ts.map +1 -1
  13. package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
  14. package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts +17 -20
  15. package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
  16. package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts +63 -4
  17. package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
  18. package/dist/ir-to-client-js/control-flow.d.ts +1 -1
  19. package/dist/ir-to-client-js/element-refs.d.ts +8 -3
  20. package/dist/ir-to-client-js/element-refs.d.ts.map +1 -1
  21. package/dist/ir-to-client-js/emit-reactive.d.ts +8 -1
  22. package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
  23. package/dist/ir-to-client-js/html-template.d.ts +24 -0
  24. package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
  25. package/dist/ir-to-client-js/imports.d.ts +2 -2
  26. package/dist/ir-to-client-js/imports.d.ts.map +1 -1
  27. package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
  28. package/dist/ir-to-client-js/types.d.ts +9 -1
  29. package/dist/ir-to-client-js/types.d.ts.map +1 -1
  30. package/dist/ir-to-client-js/utils.d.ts +1 -1
  31. package/dist/types.d.ts +25 -2
  32. package/dist/types.d.ts.map +1 -1
  33. package/package.json +2 -2
  34. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +398 -671
  35. package/src/__tests__/child-components-in-map.test.ts +4 -4
  36. package/src/__tests__/client-js-generation.test.ts +19 -11
  37. package/src/__tests__/composite-branch-loop.test.ts +2 -2
  38. package/src/__tests__/cross-file-client-signal.test.ts +5 -1
  39. package/src/__tests__/early-return-scope-var-ref.test.ts +10 -11
  40. package/src/__tests__/inline-jsx-callback.test.ts +6 -4
  41. package/src/__tests__/ir-jsx-props.test.ts +6 -5
  42. package/src/__tests__/loop-fallback-wrap.test.ts +3 -4
  43. package/src/__tests__/module-level-signal.audit.test.ts +7 -2
  44. package/src/__tests__/nested-loop-conditional.test.ts +9 -7
  45. package/src/__tests__/nested-loop-reactive-attrs.test.ts +2 -2
  46. package/src/__tests__/preamble-region-patch.test.ts +18 -12
  47. package/src/__tests__/profile-cond-binding-ids.test.ts +3 -2
  48. package/src/__tests__/profile-loop-binding-ids.test.ts +4 -3
  49. package/src/__tests__/profile-nested-binding-ids.test.ts +11 -6
  50. package/src/__tests__/reactive-attrs-in-map.test.ts +6 -4
  51. package/src/__tests__/template-closure.test.ts +1 -1
  52. package/src/compiler.ts +11 -0
  53. package/src/index.ts +1 -0
  54. package/src/ir-to-client-js/client-only-elision.ts +273 -0
  55. package/src/ir-to-client-js/collect-elements.ts +8 -4
  56. package/src/ir-to-client-js/control-flow/shared.ts +1 -1
  57. package/src/ir-to-client-js/control-flow/stringify/branch-loop.ts +12 -4
  58. package/src/ir-to-client-js/control-flow/stringify/claim-plan.ts +68 -0
  59. package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +18 -9
  60. package/src/ir-to-client-js/control-flow/stringify/insert.ts +17 -11
  61. package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +35 -16
  62. package/src/ir-to-client-js/control-flow/stringify/loop.ts +48 -46
  63. package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +241 -31
  64. package/src/ir-to-client-js/control-flow.ts +1 -1
  65. package/src/ir-to-client-js/element-refs.ts +9 -11
  66. package/src/ir-to-client-js/emit-reactive.ts +50 -14
  67. package/src/ir-to-client-js/html-template.ts +29 -9
  68. package/src/ir-to-client-js/imports.ts +7 -4
  69. package/src/ir-to-client-js/reactivity.ts +3 -2
  70. package/src/ir-to-client-js/types.ts +10 -2
  71. package/src/ir-to-client-js/utils.ts +1 -1
  72. package/src/jsx-to-ir.ts +8 -7
  73. package/src/types.ts +25 -2
@@ -126,7 +126,11 @@ export function createTemplateAwareStringProtector(): {
126
126
  return { protect, restore, replaceProtectedCall }
127
127
  }
128
128
 
129
- const VOID_ELEMENTS = new Set([
129
+ // Exported (not just module-local) so `client-only-elision.ts` (slot
130
+ // unification Step B) can reuse the exact same void-element list when it
131
+ // walks the IR tree computing markerless-eligible child-index paths —
132
+ // duplicating this list would risk the two walks silently drifting apart.
133
+ export const VOID_ELEMENTS = new Set([
130
134
  'area', 'base', 'br', 'col', 'embed', 'hr', 'img',
131
135
  'input', 'link', 'meta', 'param', 'source', 'track', 'wbr',
132
136
  ])
@@ -794,6 +798,16 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
794
798
 
795
799
  case 'expression': {
796
800
  if (node.expr === 'null' || node.expr === 'undefined') return ''
801
+ // Slot unification Step B (`markerless`, decided once by
802
+ // `client-only-elision.ts` before this CSR pass runs): drop the
803
+ // marker pair and fall through to the same bare `${...}` shape the
804
+ // no-slotId branch below already uses — `elidedPath` (not this
805
+ // string) is what the claim plan resolves against, so no marker
806
+ // anchor is needed here at all.
807
+ if (node.markerless) {
808
+ const bare = wrapInterpolation(wrapExpr(node.expr))
809
+ return `\${${bare}}`
810
+ }
797
811
  const inner = wrapInterpolation(wrapExpr(node.expr))
798
812
  // Stage 3 / D4 — an element-array child ({out}) built by an arbitrary
799
813
  // .map() preamble is an array of HTML strings; join it rather than let
@@ -908,8 +922,8 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
908
922
  // (Go's `bf_sort`, etc.) applied the sort separately and
909
923
  // hydration only needed to match, but broken on Hono / CSR
910
924
  // where the template is the only source of truth. The chain
911
- // mirrors `buildChainedArrayExpr` so reconcileList sees the
912
- // same array shape this template emits.
925
+ // mirrors `buildChainedArrayExpr` so mapArray/mapArrayAnchored sees
926
+ // the same array shape this template emits.
913
927
  const rawChainedArray = applyLoopChain(node)
914
928
  const { array: iterArray, callbackParam } = applyIterationShape(node, rawChainedArray, indexParam)
915
929
  const wrappedArray = wrapExpr(iterArray)
@@ -1120,7 +1134,9 @@ export interface SkeletonSlotPaths {
1120
1134
  * the loop keeps its hoisted-clone fast path, but every slot lookup falls
1121
1135
  * back to `qsa`/`$t`.
1122
1136
  */
1123
- const SKELETON_PATH_HAZARD_TAGS = new Set([
1137
+ // Exported for reuse by `client-only-elision.ts` (Step B) — same hazard
1138
+ // class, same reasoning, must never drift out of sync with a second copy.
1139
+ export const SKELETON_PATH_HAZARD_TAGS = new Set([
1124
1140
  'table', 'thead', 'tbody', 'tfoot', 'caption', 'colgroup', 'col',
1125
1141
  'select', 'optgroup',
1126
1142
  'p',
@@ -1136,7 +1152,7 @@ const SKELETON_PATH_HAZARD_TAGS = new Set([
1136
1152
  * `<dt>` closes the `<dt>`). A skeleton hitting this bails on path
1137
1153
  * computation entirely (see `SKELETON_PATH_HAZARD_TAGS` doc).
1138
1154
  */
1139
- const SKELETON_PATH_FORCE_CLOSE_GROUPS: ReadonlyArray<ReadonlySet<string>> = [
1155
+ export const SKELETON_PATH_FORCE_CLOSE_GROUPS: ReadonlyArray<ReadonlySet<string>> = [
1140
1156
  new Set(['a']),
1141
1157
  new Set(['button']),
1142
1158
  new Set(['form']),
@@ -1146,7 +1162,7 @@ const SKELETON_PATH_FORCE_CLOSE_GROUPS: ReadonlyArray<ReadonlySet<string>> = [
1146
1162
  new Set(['li']),
1147
1163
  ]
1148
1164
 
1149
- function skeletonForceCloseGroup(tag: string): number {
1165
+ export function skeletonForceCloseGroup(tag: string): number {
1150
1166
  return SKELETON_PATH_FORCE_CLOSE_GROUPS.findIndex(group => group.has(tag))
1151
1167
  }
1152
1168
 
@@ -1204,7 +1220,7 @@ function walkSkeletonPathNode(
1204
1220
  }
1205
1221
 
1206
1222
  /** True if `children` (already flattened) contains anything the HTML parser would foster-parent out of a `<tr>`. */
1207
- function hasForeignTableRowContent(children: readonly IRNode[]): boolean {
1223
+ export function hasForeignTableRowContent(children: readonly IRNode[]): boolean {
1208
1224
  for (const child of children) {
1209
1225
  if (child.type === 'text') {
1210
1226
  if (child.value.trim() !== '') return true
@@ -1218,7 +1234,7 @@ function hasForeignTableRowContent(children: readonly IRNode[]): boolean {
1218
1234
  }
1219
1235
 
1220
1236
  /** Splice fragment children inline — a fragment contributes no DOM node of its own. */
1221
- function flattenSkeletonChildren(children: readonly IRNode[]): IRNode[] {
1237
+ export function flattenSkeletonChildren(children: readonly IRNode[]): IRNode[] {
1222
1238
  const out: IRNode[] = []
1223
1239
  for (const child of children) {
1224
1240
  if (child.type === 'fragment') {
@@ -2322,7 +2338,11 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
2322
2338
  case 'expression':
2323
2339
  if (node.expr === 'null' || node.expr === 'undefined') return ''
2324
2340
  if (node.clientOnly && node.slotId) {
2325
- return `<!--bf-client:${node.slotId}--><!--/-->`
2341
+ // Ordinary claimed 'text' slot pair (slot unification A3) — matches
2342
+ // the SSR adapters' `renderExpression` byte-for-byte (byte parity):
2343
+ // empty at CSR mount too, since the expression is evaluated only
2344
+ // once init's createEffect runs, same as the SSR case.
2345
+ return `<!--bf:${node.slotId}--><!--/-->`
2326
2346
  }
2327
2347
  {
2328
2348
  const transformed = transformExpr(node.expr, node.templateExpr)
@@ -8,13 +8,16 @@ import { isClientBuiltinName } from '../builtins.ts'
8
8
  // All exports from @barefootjs/client/runtime that may be used in generated code
9
9
  export const RUNTIME_IMPORT_CANDIDATES = [
10
10
  'createSignal', 'createMemo', 'createEffect', 'onCleanup', 'onMount',
11
- 'hydrate', 'insert', 'reconcileElements', 'getLoopChildren', 'getLoopNodes', 'mapArray', 'mapArrayAnchored', 'patchLeaf', 'patchSlotRange', 'createDisposableEffect',
12
- 'createComponent', 'renderChild', 'registerComponent', 'registerTemplate', 'initChild', 'upsertChild', 'updateClientMarker',
11
+ 'hydrate', 'insert', 'getLoopChildren', 'getLoopNodes', 'mapArray', 'mapArrayAnchored', 'patchLeaf', 'createDisposableEffect',
12
+ 'createComponent', 'renderChild', 'registerComponent', 'registerTemplate', 'initChild', 'upsertChild',
13
13
  'createPortal',
14
14
  'provideContext', 'createContext', 'useContext',
15
- 'forwardProps', 'applyRestAttrs', 'splitProps', 'spreadAttrs', 'styleToCss', 'escapeAttr', 'escapeText',
15
+ 'forwardProps', 'applyRestAttrs', 'splitProps', 'spreadAttrs', 'styleToCss', 'escapeAttr', 'escapeText', 'escapeTextOrNode',
16
16
  'qsa', 'qsaItem', 'qsaChildScope', 'qsaChildScopes', 'upsertChildItem', '__slot', '__bfSlot', '__bfText',
17
- 'tAfter',
17
+ // Claim-plan interpreter (slot unification A2/A3, spec/slot-unification.md)
18
+ // — the "one claim mechanism" that replaced `patchSlotRange` and
19
+ // `updateClientMarker` (both deleted) as the content-slot update door.
20
+ 'claimSlots', 'lazySlots',
18
21
  // Profile mode (#1690, SR3) — turn-boundary markers around event handlers.
19
22
  'beginTurn', 'endTurn',
20
23
  // Catalogued `Date` lowering (#2274/#2292) — the client counterpart to
@@ -571,8 +571,9 @@ export function collectLoopChildReactiveTexts(
571
571
  expression: ({ node: n, scope: insideConditional }) => {
572
572
  if (!n.slotId) return
573
573
  // #2389 — a preamble-patched region (see `IRLoop.preambleRegions`) has
574
- // its own `patchSlotRange`-based effect (`preambleRegions` in the
575
- // client-JS loop plan); it must never ALSO become a `reactiveTexts`
574
+ // its own claimed 'markup'-slot effect (`preambleRegions` in the
575
+ // client-JS loop plan; slot unification A3, `@barefootjs/client/
576
+ // runtime/claim-slots.ts`); it must never ALSO become a `reactiveTexts`
576
577
  // entry, which would patch it via `.textContent` and corrupt markup
577
578
  // (a `joinArrayChild` region's value is raw HTML, not text).
578
579
  if (n.preambleRegion) return
@@ -255,7 +255,7 @@ export interface LoopCore {
255
255
  /**
256
256
  * Unique id for this loop's `<!--bf-loop:<id>--> ... <!--bf-/loop:<id>-->`
257
257
  * marker pair, threaded from `IRLoop.markerId`. Each `mapArray()` /
258
- * `reconcileElements()` call passes this id so sibling loops under the
258
+ * `mapArrayAnchored()` call passes this id so sibling loops under the
259
259
  * same parent reconcile their own range (#1087).
260
260
  */
261
261
  markerId: string
@@ -609,7 +609,7 @@ export interface TopLevelLoop extends LoopCore {
609
609
  // Per-item bindings (events / reactiveAttrs / reactiveTexts / refs / conditionals)
610
610
  // now live on `LoopCore.bindings` — see issue #1244 §B.
611
611
  isStaticArray: boolean // True if array is a static prop (not a signal)
612
- useElementReconciliation?: boolean // True: reconcileElements + composite rendering (native root with child components)
612
+ useElementReconciliation?: boolean // True: mapArray/mapArrayAnchored + composite rendering (native root with child components)
613
613
  /** Inner loop metadata for composite element reconciliation (array, param, key, container) */
614
614
  innerLoops?: NestedLoop[]
615
615
  /** Offset of this loop's items past its preceding container siblings (#1693). */
@@ -654,6 +654,14 @@ export interface ReactiveAttribute extends AttrMeta {
654
654
  export interface ClientOnlyElement {
655
655
  slotId: string
656
656
  expression: string
657
+ /**
658
+ * Slot unification Step B (`spec/slot-unification.md` §3(b), §5 Step B):
659
+ * copied from `IRExpression.elidedPath` (`client-only-elision.ts`) when
660
+ * the compiler proved this slot's marker pair can be elided. Absent
661
+ * (`undefined`) is always safe — the claim plan falls back to `path: []`
662
+ * / marker-based resolution exactly as before Step B.
663
+ */
664
+ elidedPath?: readonly number[]
657
665
  }
658
666
 
659
667
 
@@ -180,7 +180,7 @@ export function applyObjectIterationWrap(
180
180
  }
181
181
 
182
182
  /**
183
- * Build the chained array expression for reconcileList. Thin
183
+ * Build the chained array expression for mapArray/mapArrayAnchored. Thin
184
184
  * adapter over `buildLoopChainExpr` that unpacks the collected
185
185
  * `TopLevelLoop` / `BranchLoop` shape into the primitive inputs.
186
186
  * Branch loops carry the same `filterPredicate` / `sortComparator`
package/src/jsx-to-ir.ts CHANGED
@@ -4610,8 +4610,8 @@ function transformMapCall(
4610
4610
  const bodyIsMultiRoot = loopBodyIsMultiRoot(children)
4611
4611
 
4612
4612
  // Determine if array is static (prop) or dynamic (signal/memo).
4613
- // Static arrays don't need reconcileList — SSR elements are hydrated
4614
- // directly. Signal / memo arrays need reconcileList for dynamic DOM
4613
+ // Static arrays don't need mapArray/mapArrayAnchored — SSR elements are
4614
+ // hydrated directly. Signal / memo arrays need mapArray for dynamic DOM
4615
4615
  // updates.
4616
4616
  //
4617
4617
  // Solid-style wrap-by-default fallback (#943, follow-up to
@@ -4621,7 +4621,7 @@ function transformMapCall(
4621
4621
  // where `getItems` is an imported helper previously silent-dropped
4622
4622
  // into the static-render path, freezing the SSR-time list on the
4623
4623
  // client. Over-reconciling an array that happens to contain a pure
4624
- // call costs one extra `reconcileList` per loop; under-reconciling
4624
+ // call costs one extra `mapArray` per loop; under-reconciling
4625
4625
  // is the silent-drop bug this closes.
4626
4626
  //
4627
4627
  // Destructured map params (`([, cfg]) => ...`, `({ a, b }) => ...`,
@@ -4660,8 +4660,9 @@ function transformMapCall(
4660
4660
 
4661
4661
  // Collect nested components for both static and dynamic arrays.
4662
4662
  // Static arrays: needed for initChild hydration.
4663
- // Dynamic arrays with native root + component descendants: enables reconcileElements
4664
- // with composite rendering (placeholder + createComponent replacement).
4663
+ // Dynamic arrays with native root + component descendants: enables
4664
+ // mapArray/mapArrayAnchored with composite rendering (placeholder +
4665
+ // createComponent replacement).
4665
4666
  const nestedComponents = collectNestedComponents(children).filter(c => c.name !== childComponent?.name)
4666
4667
 
4667
4668
  return {
@@ -6564,7 +6565,7 @@ function isArrayExprDirectPropRef(arrayExpr: ts.Expression, ctx: TransformContex
6564
6565
 
6565
6566
  /**
6566
6567
  * Check if array expression is a signal or memo getter call.
6567
- * Used to determine if a loop needs reconcileList for dynamic DOM updates.
6568
+ * Used to determine if a loop needs mapArray for dynamic DOM updates.
6568
6569
  * Props and local constants are considered static (don't change at runtime).
6569
6570
  */
6570
6571
  function isSignalOrMemoArray(array: string, ctx: TransformContext): boolean {
@@ -6767,7 +6768,7 @@ function attrValueReactivityProbe(value: AttrValue): string | null {
6767
6768
 
6768
6769
  /**
6769
6770
  * Propagate slotId to loop children that need it.
6770
- * Loops need to use their parent element's slotId for reconcileList.
6771
+ * Loops need to use their parent element's slotId for mapArray.
6771
6772
  * This handles loops directly in children or nested in fragments.
6772
6773
  */
6773
6774
  function propagateSlotIdToLoops(children: IRNode[], slotId: string): void {
package/src/types.ts CHANGED
@@ -386,7 +386,8 @@ export interface IRExpression {
386
386
  * intersect the enclosing loop's `preamble.declaredNames`. Excludes the
387
387
  * node from `collectLoopChildReactiveTexts` (which patches via
388
388
  * `.textContent`, wrong for markup) regardless of `reactive` / `slotId` —
389
- * the region-patch effect (`patchSlotRange`) is its only wiring.
389
+ * the region-patch effect (a claimed 'markup' slot writer, slot
390
+ * unification A3) is its only wiring.
390
391
  */
391
392
  preambleRegion?: boolean
392
393
  /** When true, expression calls signal getters or memos (has reactive `foo()` pattern). */
@@ -400,6 +401,28 @@ export interface IRExpression {
400
401
  * source of truth for free-reference / reactivity classification.
401
402
  */
402
403
  origin: OriginInfo
404
+ /**
405
+ * Slot unification Step B (`spec/slot-unification.md` §3(b), §5 Step B):
406
+ * true when this slot's `<!--bf:sN-->…<!--/-->` marker pair can be safely
407
+ * omitted from BOTH SSR and CSR output, because `elidedPath` already gives
408
+ * every claimer a real compile-time DOM path to the slot's position.
409
+ * Decided EXACTLY ONCE, by `client-only-elision.ts`, before either
410
+ * `adapter.generate()` (SSR) or `generateClientJs()` (CSR) run — every one
411
+ * of the nine SSR adapters' `renderExpression` and the CSR template
412
+ * emitters (`html-template.ts`) read this single flag and must never
413
+ * re-derive their own elision decision. Unset/false is always safe
414
+ * (keeps markers, today's behavior); only the compiler pass above may
415
+ * ever set it true.
416
+ */
417
+ markerless?: boolean
418
+ /**
419
+ * Root-relative child-index path to this slot's position (parent index
420
+ * chain, LAST element = the index of the slot itself within its parent's
421
+ * `childNodes`). Valid, and required, only when `markerless` is true —
422
+ * see `SlotSpec.markerless` in `@barefootjs/client/runtime/claim-slots.ts`
423
+ * for the claim-time resolve-or-create semantics this path feeds.
424
+ */
425
+ elidedPath?: readonly number[]
403
426
  }
404
427
 
405
428
  export interface IRConditional {
@@ -723,7 +746,7 @@ export interface IRLoop {
723
746
  * `slotId` is emitted as the usual `<!--bf:sN-->...<!--/-->` marker pair
724
747
  * (same door as a reactive text slot — `irToHtmlTemplate` / Hono's
725
748
  * `renderExpression` key off `slotId` alone), but the CLIENT wiring is a
726
- * distinct `patchSlotRange`-based region-patch effect (`preambleRegions`
749
+ * distinct claimed 'markup'-slot region-patch effect (`preambleRegions`
727
750
  * in the client-JS loop plan), NOT a `reactiveTexts` entry — patching via
728
751
  * `.textContent` would escape markup that a `joinArrayChild` region must
729
752
  * render raw. `collectLoopChildReactiveTexts` excludes any node collected