@barefootjs/client 0.1.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 (108) hide show
  1. package/dist/build.d.ts +56 -0
  2. package/dist/build.d.ts.map +1 -0
  3. package/dist/build.js +76 -0
  4. package/dist/context.d.ts +25 -0
  5. package/dist/context.d.ts.map +1 -0
  6. package/dist/csr-adapter.d.ts +26 -0
  7. package/dist/csr-adapter.d.ts.map +1 -0
  8. package/dist/forward-props.d.ts +17 -0
  9. package/dist/forward-props.d.ts.map +1 -0
  10. package/dist/index.d.ts +8 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +154 -0
  13. package/dist/reactive.d.ts +150 -0
  14. package/dist/reactive.d.ts.map +1 -0
  15. package/dist/reactive.js +215 -0
  16. package/dist/runtime/apply-rest-attrs.d.ts +16 -0
  17. package/dist/runtime/apply-rest-attrs.d.ts.map +1 -0
  18. package/dist/runtime/branch-slot.d.ts +22 -0
  19. package/dist/runtime/branch-slot.d.ts.map +1 -0
  20. package/dist/runtime/client-marker.d.ts +21 -0
  21. package/dist/runtime/client-marker.d.ts.map +1 -0
  22. package/dist/runtime/component.d.ts +99 -0
  23. package/dist/runtime/component.d.ts.map +1 -0
  24. package/dist/runtime/context.d.ts +40 -0
  25. package/dist/runtime/context.d.ts.map +1 -0
  26. package/dist/runtime/hydrate.d.ts +100 -0
  27. package/dist/runtime/hydrate.d.ts.map +1 -0
  28. package/dist/runtime/hydration-state.d.ts +13 -0
  29. package/dist/runtime/hydration-state.d.ts.map +1 -0
  30. package/dist/runtime/index.d.ts +27 -0
  31. package/dist/runtime/index.d.ts.map +1 -0
  32. package/dist/runtime/index.js +2093 -0
  33. package/dist/runtime/insert.d.ts +75 -0
  34. package/dist/runtime/insert.d.ts.map +1 -0
  35. package/dist/runtime/list.d.ts +21 -0
  36. package/dist/runtime/list.d.ts.map +1 -0
  37. package/dist/runtime/map-array.d.ts +32 -0
  38. package/dist/runtime/map-array.d.ts.map +1 -0
  39. package/dist/runtime/portal.d.ts +96 -0
  40. package/dist/runtime/portal.d.ts.map +1 -0
  41. package/dist/runtime/qsa-item.d.ts +52 -0
  42. package/dist/runtime/qsa-item.d.ts.map +1 -0
  43. package/dist/runtime/query.d.ts +86 -0
  44. package/dist/runtime/query.d.ts.map +1 -0
  45. package/dist/runtime/reconcile-elements.d.ts +44 -0
  46. package/dist/runtime/reconcile-elements.d.ts.map +1 -0
  47. package/dist/runtime/registry.d.ts +53 -0
  48. package/dist/runtime/registry.d.ts.map +1 -0
  49. package/dist/runtime/render.d.ts +35 -0
  50. package/dist/runtime/render.d.ts.map +1 -0
  51. package/dist/runtime/scope.d.ts +28 -0
  52. package/dist/runtime/scope.d.ts.map +1 -0
  53. package/dist/runtime/slot-resolver.d.ts +36 -0
  54. package/dist/runtime/slot-resolver.d.ts.map +1 -0
  55. package/dist/runtime/spread-attrs.d.ts +19 -0
  56. package/dist/runtime/spread-attrs.d.ts.map +1 -0
  57. package/dist/runtime/standalone.js +2278 -0
  58. package/dist/runtime/streaming.d.ts +36 -0
  59. package/dist/runtime/streaming.d.ts.map +1 -0
  60. package/dist/runtime/style.d.ts +17 -0
  61. package/dist/runtime/style.d.ts.map +1 -0
  62. package/dist/runtime/template.d.ts +39 -0
  63. package/dist/runtime/template.d.ts.map +1 -0
  64. package/dist/runtime/types.d.ts +26 -0
  65. package/dist/runtime/types.d.ts.map +1 -0
  66. package/dist/shims.d.ts +21 -0
  67. package/dist/shims.d.ts.map +1 -0
  68. package/dist/slot.d.ts +14 -0
  69. package/dist/slot.d.ts.map +1 -0
  70. package/dist/split-props.d.ts +26 -0
  71. package/dist/split-props.d.ts.map +1 -0
  72. package/dist/unwrap.d.ts +16 -0
  73. package/dist/unwrap.d.ts.map +1 -0
  74. package/package.json +71 -0
  75. package/src/build.ts +92 -0
  76. package/src/context.ts +33 -0
  77. package/src/csr-adapter.ts +134 -0
  78. package/src/forward-props.ts +43 -0
  79. package/src/index.ts +42 -0
  80. package/src/reactive.ts +411 -0
  81. package/src/runtime/apply-rest-attrs.ts +109 -0
  82. package/src/runtime/branch-slot.ts +32 -0
  83. package/src/runtime/client-marker.ts +46 -0
  84. package/src/runtime/component.ts +501 -0
  85. package/src/runtime/context.ts +111 -0
  86. package/src/runtime/hydrate.ts +311 -0
  87. package/src/runtime/hydration-state.ts +13 -0
  88. package/src/runtime/index.ts +96 -0
  89. package/src/runtime/insert.ts +407 -0
  90. package/src/runtime/list.ts +47 -0
  91. package/src/runtime/map-array.ts +381 -0
  92. package/src/runtime/portal.ts +174 -0
  93. package/src/runtime/qsa-item.ts +128 -0
  94. package/src/runtime/query.ts +632 -0
  95. package/src/runtime/reconcile-elements.ts +391 -0
  96. package/src/runtime/registry.ts +160 -0
  97. package/src/runtime/render.ts +105 -0
  98. package/src/runtime/scope.ts +46 -0
  99. package/src/runtime/slot-resolver.ts +66 -0
  100. package/src/runtime/spread-attrs.ts +88 -0
  101. package/src/runtime/streaming.ts +65 -0
  102. package/src/runtime/style.ts +27 -0
  103. package/src/runtime/template.ts +53 -0
  104. package/src/runtime/types.ts +27 -0
  105. package/src/shims.ts +54 -0
  106. package/src/slot.ts +23 -0
  107. package/src/split-props.ts +86 -0
  108. package/src/unwrap.ts +18 -0
@@ -0,0 +1,411 @@
1
+ /**
2
+ * BarefootJS - Reactive Primitives
3
+ *
4
+ * Minimal reactive system for DOM manipulation.
5
+ * Inspired by SolidJS signals.
6
+ */
7
+
8
+ /**
9
+ * Phantom brand for compile-time reactivity detection.
10
+ * The compiler checks for the '__reactive' property via TypeChecker
11
+ * to identify reactive expressions.
12
+ */
13
+ export type Reactive<T> = T & { readonly __reactive: true }
14
+
15
+ export type Signal<T> = [
16
+ /** Get current value (registers dependency when called inside effect) */
17
+ Reactive<() => T>,
18
+ /** Update value (accepts value or updater function) */
19
+ (valueOrFn: T | ((prev: T) => T)) => void
20
+ ]
21
+
22
+ export type CleanupFn = () => void
23
+ export type EffectFn = () => void | CleanupFn
24
+ export type Memo<T> = Reactive<() => T>
25
+
26
+ type EffectContext = {
27
+ fn: EffectFn
28
+ cleanup: CleanupFn | null
29
+ dependencies: Set<Set<EffectContext>>
30
+ owner: EffectContext | null // Parent scope for hierarchical disposal
31
+ children: EffectContext[] // Owned child effects/roots
32
+ disposed: boolean
33
+ runCount: number // Per-effect re-entry counter for circular dependency detection
34
+ }
35
+
36
+ let Owner: EffectContext | null = null
37
+ let Listener: EffectContext | null = null
38
+ const MAX_EFFECT_RUNS = 100
39
+
40
+ let BatchDepth = 0
41
+ const PendingEffects = new Set<EffectContext>()
42
+
43
+ /**
44
+ * Create a reactive value
45
+ *
46
+ * @param initialValue - Initial value
47
+ * @returns [getter, setter] tuple
48
+ *
49
+ * @example
50
+ * const [count, setCount] = createSignal(0)
51
+ * count() // 0
52
+ * setCount(5) // Update to 5
53
+ * setCount(n => n + 1) // Update with function (becomes 6)
54
+ */
55
+ export function createSignal<T>(initialValue: T): Signal<T> {
56
+ let value = initialValue
57
+ const subscribers = new Set<EffectContext>()
58
+
59
+ const get = () => {
60
+ if (Listener) {
61
+ subscribers.add(Listener)
62
+ Listener.dependencies.add(subscribers)
63
+ }
64
+ return value
65
+ }
66
+
67
+ const set = (valueOrFn: T | ((prev: T) => T)) => {
68
+ const newValue = typeof valueOrFn === 'function'
69
+ ? (valueOrFn as (prev: T) => T)(value)
70
+ : valueOrFn
71
+
72
+ if (Object.is(value, newValue)) {
73
+ return
74
+ }
75
+
76
+ value = newValue
77
+
78
+ if (BatchDepth > 0) {
79
+ for (const effect of subscribers) {
80
+ PendingEffects.add(effect)
81
+ }
82
+ } else {
83
+ const effectsToRun = [...subscribers]
84
+ for (const effect of effectsToRun) {
85
+ runEffect(effect)
86
+ }
87
+ }
88
+ }
89
+
90
+ return [get, set] as Signal<T>
91
+ }
92
+
93
+ /**
94
+ * Side effect that runs automatically when signals change
95
+ *
96
+ * @param fn - Effect function (can return a cleanup function)
97
+ *
98
+ * @example
99
+ * const [count, setCount] = createSignal(0)
100
+ * createEffect(() => {
101
+ * console.log("count changed:", count())
102
+ * })
103
+ * setCount(1) // Logs "count changed: 1"
104
+ */
105
+ export function createEffect(fn: EffectFn): void {
106
+ // Note: Nested effects are now allowed. runEffect() properly saves/restores
107
+ // prevEffect, so nested effects correctly track their own dependencies.
108
+ // This enables synchronous component initialization in reconcileList.
109
+
110
+ const effect: EffectContext = {
111
+ fn,
112
+ cleanup: null,
113
+ dependencies: new Set(),
114
+ owner: Owner,
115
+ children: [],
116
+ disposed: false,
117
+ runCount: 0,
118
+ }
119
+
120
+ // Register with parent owner for hierarchical disposal
121
+ if (Owner) Owner.children.push(effect)
122
+
123
+ runEffect(effect)
124
+ }
125
+
126
+ function runEffect(effect: EffectContext): void {
127
+ if (effect.disposed) return
128
+
129
+ effect.runCount++
130
+ if (effect.runCount > MAX_EFFECT_RUNS) {
131
+ effect.runCount = 0
132
+ throw new Error(`Circular dependency detected: effect re-entered itself ${MAX_EFFECT_RUNS} times.`)
133
+ }
134
+
135
+ if (effect.cleanup) {
136
+ effect.cleanup()
137
+ effect.cleanup = null
138
+ }
139
+
140
+ for (const dep of effect.dependencies) {
141
+ dep.delete(effect)
142
+ }
143
+ effect.dependencies.clear()
144
+
145
+ const prevOwner = Owner
146
+ const prevListener = Listener
147
+ Owner = effect
148
+ Listener = effect
149
+
150
+ try {
151
+ const result = effect.fn()
152
+ if (typeof result === 'function') {
153
+ effect.cleanup = result
154
+ }
155
+ } finally {
156
+ Owner = prevOwner
157
+ Listener = prevListener
158
+ effect.runCount--
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Dispose an effect and its entire owned subtree, without touching the
164
+ * parent's `children` list. Internal to `disposeEffect` — every recursive
165
+ * step uses this path so cascade disposal can never mutate a list it's
166
+ * currently iterating over.
167
+ */
168
+ function disposeSubtree(effect: EffectContext): void {
169
+ if (effect.disposed) return
170
+ effect.disposed = true
171
+
172
+ for (const child of effect.children) {
173
+ disposeSubtree(child)
174
+ }
175
+ effect.children.length = 0
176
+
177
+ if (effect.cleanup) {
178
+ effect.cleanup()
179
+ effect.cleanup = null
180
+ }
181
+
182
+ for (const dep of effect.dependencies) {
183
+ dep.delete(effect)
184
+ }
185
+ effect.dependencies.clear()
186
+
187
+ effect.owner = null
188
+ }
189
+
190
+ /**
191
+ * Public dispose entry point: detach `effect` from its parent's `children`
192
+ * list, then dispose the subtree rooted at `effect`. The detach step lives
193
+ * only here so the recursion (which doesn't need it — the parent clears
194
+ * `children.length = 0` itself) cannot splice entries out of the array
195
+ * it is iterating. The splice-during-iter shape was the root cause of
196
+ * #1366; separating the two responsibilities makes it structurally
197
+ * unreachable.
198
+ */
199
+ function disposeEffect(effect: EffectContext): void {
200
+ if (effect.disposed) return
201
+
202
+ if (effect.owner) {
203
+ const idx = effect.owner.children.indexOf(effect)
204
+ if (idx >= 0) effect.owner.children.splice(idx, 1)
205
+ }
206
+
207
+ disposeSubtree(effect)
208
+ }
209
+
210
+ /**
211
+ * Create an isolated reactive scope with explicit disposal.
212
+ * All effects/memos created inside run within this root and are
213
+ * disposed together when the returned dispose function is called.
214
+ *
215
+ * Used internally by mapArray for per-item reactive scopes.
216
+ *
217
+ * @param fn - Function to run in the new scope. Receives a dispose function.
218
+ * @returns The return value of fn
219
+ */
220
+ export function createRoot<T>(fn: (dispose: () => void) => T): T {
221
+ const root: EffectContext = {
222
+ fn: () => {},
223
+ cleanup: null,
224
+ dependencies: new Set(),
225
+ owner: Owner,
226
+ children: [],
227
+ disposed: false,
228
+ runCount: 0,
229
+ }
230
+
231
+ if (Owner) Owner.children.push(root)
232
+
233
+ const prevOwner = Owner
234
+ const prevListener = Listener
235
+ Owner = root
236
+ Listener = null // Isolate: signal reads inside root don't track in parent effect
237
+
238
+ try {
239
+ return fn(() => disposeEffect(root))
240
+ } finally {
241
+ Owner = prevOwner
242
+ Listener = prevListener
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Create an effect that can be explicitly disposed (unsubscribed from all signals).
248
+ * Used for effects inside conditional branches that need cleanup on branch switch.
249
+ *
250
+ * @returns A dispose function that stops the effect and removes it from all signal dependencies.
251
+ */
252
+ export function createDisposableEffect(fn: EffectFn): () => void {
253
+ let disposed = false
254
+
255
+ const effect: EffectContext = {
256
+ fn: () => {
257
+ if (disposed) return // Prevent re-activation after disposal
258
+ return fn()
259
+ },
260
+ cleanup: null,
261
+ dependencies: new Set(),
262
+ owner: Owner,
263
+ children: [],
264
+ disposed: false,
265
+ runCount: 0,
266
+ }
267
+
268
+ if (Owner) Owner.children.push(effect)
269
+
270
+ runEffect(effect)
271
+
272
+ return () => {
273
+ disposeEffect(effect)
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Register cleanup function for effects
279
+ *
280
+ * @param fn - Cleanup function
281
+ *
282
+ * @example
283
+ * createEffect(() => {
284
+ * const timer = setInterval(() => console.log('tick'), 1000)
285
+ * onCleanup(() => clearInterval(timer))
286
+ * })
287
+ */
288
+ export function onCleanup(fn: CleanupFn): void {
289
+ if (Owner) {
290
+ const effect = Owner
291
+ const prevCleanup = effect.cleanup
292
+ effect.cleanup = () => {
293
+ if (prevCleanup) prevCleanup()
294
+ fn()
295
+ }
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Run a function without tracking signal dependencies
301
+ *
302
+ * @param fn - Function to run without tracking
303
+ * @returns The return value of fn
304
+ *
305
+ * @example
306
+ * createEffect(() => {
307
+ * const value = untrack(() => someSignal()) // won't re-run when someSignal changes
308
+ * console.log(value)
309
+ * })
310
+ */
311
+ export function untrack<T>(fn: () => T): T {
312
+ const prevListener = Listener
313
+ Listener = null
314
+ try {
315
+ return fn()
316
+ } finally {
317
+ Listener = prevListener
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Batch multiple signal updates and propagate once
323
+ *
324
+ * Collects all signal writes inside `fn`, then flushes
325
+ * dependent effects after `fn` returns. Duplicate effects
326
+ * are deduplicated, so a deep memo chain only propagates once
327
+ * regardless of how many times the source signal was written.
328
+ *
329
+ * Batches can be nested — effects flush when the outermost batch ends.
330
+ *
331
+ * @param fn - Function containing signal writes to batch
332
+ * @returns The return value of fn
333
+ *
334
+ * @example
335
+ * const [a, setA] = createSignal(0)
336
+ * const [b, setB] = createSignal(0)
337
+ * batch(() => {
338
+ * setA(1) // queued
339
+ * setB(2) // queued
340
+ * })
341
+ * // effects run once here, not twice
342
+ */
343
+ export function batch<T>(fn: () => T): T {
344
+ BatchDepth++
345
+ try {
346
+ return fn()
347
+ } finally {
348
+ BatchDepth--
349
+ if (BatchDepth === 0) {
350
+ flushEffects()
351
+ }
352
+ }
353
+ }
354
+
355
+ function flushEffects(): void {
356
+ while (PendingEffects.size > 0) {
357
+ const effects = [...PendingEffects]
358
+ PendingEffects.clear()
359
+ for (const effect of effects) {
360
+ runEffect(effect)
361
+ }
362
+ }
363
+ }
364
+
365
+ /**
366
+ * Run a function once when the component mounts
367
+ *
368
+ * Thin wrapper around createEffect for one-time mount code.
369
+ * The function runs immediately and does not track any dependencies.
370
+ *
371
+ * @param fn - Function to run on mount
372
+ *
373
+ * @example
374
+ * onMount(() => {
375
+ * console.log('Component mounted!')
376
+ * onCleanup(() => console.log('Component unmounted!'))
377
+ * })
378
+ */
379
+ export function onMount(fn: () => void): void {
380
+ createEffect(() => untrack(fn))
381
+ }
382
+
383
+ /**
384
+ * Create a memoized computed value
385
+ *
386
+ * A derived signal that:
387
+ * - Tracks dependencies automatically (like createEffect)
388
+ * - Caches the computed result
389
+ * - Acts as a read-only signal (can be used as dependency by other effects/memos)
390
+ *
391
+ * @param fn - Computation function that returns a value
392
+ * @returns Getter function for the memoized value
393
+ *
394
+ * @example
395
+ * const [count, setCount] = createSignal(2)
396
+ * const doubled = createMemo(() => count() * 2)
397
+ * doubled() // 4
398
+ * setCount(5)
399
+ * doubled() // 10
400
+ */
401
+ export function createMemo<T>(fn: () => T): Memo<T> {
402
+ const [value, setValue] = createSignal<T>(undefined as T)
403
+
404
+ createEffect(() => {
405
+ const result = fn()
406
+ setValue(() => result)
407
+ })
408
+
409
+ return value
410
+ }
411
+
@@ -0,0 +1,109 @@
1
+ /**
2
+ * BarefootJS - Apply Rest Attributes Helper
3
+ *
4
+ * Applies spread attributes to HTML elements at hydration time.
5
+ * Used when spread props cannot be statically expanded (open types).
6
+ */
7
+
8
+ import { createEffect } from '@barefootjs/client/reactive'
9
+ import { styleToCss } from './style'
10
+
11
+ /** Map of JSX prop names to HTML attribute names */
12
+ function toAttrName(key: string): string {
13
+ if (key === 'className') return 'class'
14
+ if (key === 'htmlFor') return 'for'
15
+ // Convert camelCase to kebab-case for data-* and aria-* style attributes
16
+ return key.replace(/([A-Z])/g, '-$1').toLowerCase()
17
+ }
18
+
19
+ /**
20
+ * Convert a JSX event prop name to a DOM event name for addEventListener.
21
+ * Handles: camelCase → lowercase, plus special mappings (doubleclick → dblclick).
22
+ * Mirrors the compiler's toDomEventName in packages/jsx/src/ir-to-client-js/utils.ts.
23
+ */
24
+ const jsxToDomEventMap: Record<string, string> = { doubleclick: 'dblclick' }
25
+ function toEventName(jsxPropName: string): string {
26
+ // onKeyDown → 'k' + 'eyDown' → 'keydown'
27
+ const raw = (jsxPropName[2].toLowerCase() + jsxPropName.slice(3)).toLowerCase()
28
+ return jsxToDomEventMap[raw] ?? raw
29
+ }
30
+
31
+ /**
32
+ * Reactively apply rest attributes from a props source onto an HTML element.
33
+ * Runs inside a createEffect so attribute values update when props change.
34
+ *
35
+ * @param el - The target DOM element
36
+ * @param source - The props/rest object to read attributes from
37
+ * @param excludeKeys - Keys already handled statically (don't apply twice)
38
+ */
39
+ export function applyRestAttrs(
40
+ el: Element,
41
+ source: Record<string, unknown>,
42
+ excludeKeys: string[]
43
+ ): void {
44
+ const exclude = new Set(excludeKeys)
45
+
46
+ // Wire up event handlers and ref callbacks once (not reactively)
47
+ for (const key of Object.keys(source)) {
48
+ if (exclude.has(key)) continue
49
+ if (key === 'ref') {
50
+ const ref = source[key]
51
+ if (typeof ref === 'function') (ref as (el: Element) => void)(el)
52
+ continue
53
+ }
54
+ if (key.startsWith('on') && key.length > 2 && key[2] === key[2].toUpperCase()) {
55
+ const handler = source[key]
56
+ if (typeof handler === 'function') {
57
+ el.addEventListener(toEventName(key), handler as EventListener)
58
+ }
59
+ }
60
+ }
61
+
62
+ createEffect(() => {
63
+ for (const key of Object.keys(source)) {
64
+ if (exclude.has(key)) continue
65
+
66
+ // Event handlers and ref are wired up above, not as attributes
67
+ if (key === 'ref') continue
68
+ // `children` is a JSX construct rendered inside the element, never
69
+ // a DOM attribute. Without this exclusion, parent components that
70
+ // pass `children` through `{...props}` end up with
71
+ // `children="<p ...>...</p>"` written as a literal attribute on
72
+ // the wrapper div. The matching `spreadAttrs` (SSR-string) path
73
+ // already skips `children` for the same reason.
74
+ if (key === 'children') continue
75
+ if (key.startsWith('on') && key.length > 2 && key[2] === key[2].toUpperCase()) continue
76
+
77
+ const value = source[key]
78
+ const attr = toAttrName(key)
79
+
80
+ if (value != null && value !== false) {
81
+ // Use DOM property for value/checked (setAttribute sets the default, not current)
82
+ if (attr === 'value' && 'value' in el) {
83
+ const strVal = String(value)
84
+ if ((el as HTMLInputElement).value !== strVal) (el as HTMLInputElement).value = strVal
85
+ } else if (attr === 'checked' && 'checked' in el) {
86
+ (el as HTMLInputElement).checked = !!value
87
+ } else if (attr === 'style') {
88
+ // Route the `style` prop through `styleToCss` so object literals
89
+ // (`{'--err': errorHue()}`) and inline strings (`'color:red'`)
90
+ // both reach the DOM as a real CSS string instead of
91
+ // `[object Object]`. Mirrors the compiler's
92
+ // `setAttribute('style', styleToCss(...))` path used when the
93
+ // attribute is bound directly on a JSX element.
94
+ const css = styleToCss(value)
95
+ if (css == null) el.removeAttribute('style')
96
+ else el.setAttribute('style', css)
97
+ } else {
98
+ el.setAttribute(attr, String(value))
99
+ }
100
+ } else {
101
+ if (attr === 'checked' && 'checked' in el) {
102
+ (el as HTMLInputElement).checked = false
103
+ } else {
104
+ el.removeAttribute(attr)
105
+ }
106
+ }
107
+ }
108
+ })
109
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Branch-template slot helper (#1213).
3
+ *
4
+ * Conditional `template()` arrows interpolate Child-position expressions
5
+ * via `${expr}`. When `expr` evaluates to a live `Node` (e.g. the result
6
+ * of `_p.renderNode(node())` returning an `HTMLElement` from
7
+ * `createComponent`), the surrounding template literal coerces it via
8
+ * `Object.prototype.toString`, producing `"[object HTMLDivElement]"` and
9
+ * destroying the live node identity on hydration.
10
+ *
11
+ * `__bfSlot` intercepts the value before stringification: if it's a
12
+ * `Node`, it stashes the node into the closure-scoped `slots` array and
13
+ * returns a unique marker comment. The `insert()` runtime then walks the
14
+ * parsed fragment for those markers and splices the original node back
15
+ * in by identity (no `cloneNode`), preserving event listeners and signal
16
+ * bindings.
17
+ *
18
+ * Non-node values fall through to `String(value)` for the existing
19
+ * inline-string path.
20
+ */
21
+ export function __bfSlot(value: unknown, slots: Node[]): string {
22
+ if (value == null || value === false || value === true) return ''
23
+ if (typeof Node !== 'undefined' && value instanceof Node) {
24
+ const idx = slots.length
25
+ slots.push(value)
26
+ return `<!--bf-slot:${idx}-->`
27
+ }
28
+ if (Array.isArray(value)) {
29
+ return value.map((v) => __bfSlot(v, slots)).join('')
30
+ }
31
+ return String(value)
32
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * BarefootJS - Client Marker
3
+ *
4
+ * Update text content for @client directive expressions
5
+ * that are evaluated only on the client side.
6
+ */
7
+
8
+ /**
9
+ * Update text content for a client marker.
10
+ *
11
+ * Expects comment marker format: <!--bf-client:sX-->
12
+ * Both GoTemplateAdapter and HonoAdapter output this format for @client directives.
13
+ *
14
+ * A zero-width space (\u200B) is used as a prefix to mark text nodes managed by @client.
15
+ * This allows distinguishing managed text nodes from other content.
16
+ *
17
+ * @param scope - The component scope element to search within
18
+ * @param id - The slot ID (e.g., 's5')
19
+ * @param value - The value to display (will be converted to string)
20
+ */
21
+ export function updateClientMarker(scope: Element | null, id: string, value: unknown): void {
22
+ if (!scope) return
23
+
24
+ const marker = `bf-client:${id}`
25
+ const walker = document.createTreeWalker(scope, NodeFilter.SHOW_COMMENT)
26
+
27
+ while (walker.nextNode()) {
28
+ if (walker.currentNode.nodeValue === marker) {
29
+ const comment = walker.currentNode
30
+ let textNode = comment.nextSibling
31
+
32
+ // Check if next sibling is our managed text node (prefixed with zero-width space)
33
+ if (textNode?.nodeType !== Node.TEXT_NODE ||
34
+ !textNode.nodeValue?.startsWith('\u200B')) {
35
+ // Create new text node with zero-width space marker
36
+ textNode = document.createTextNode('\u200B' + String(value ?? ''))
37
+ // Insert after the comment node
38
+ comment.parentNode?.insertBefore(textNode, comment.nextSibling)
39
+ } else {
40
+ // Update existing managed text node
41
+ textNode.nodeValue = '\u200B' + String(value ?? '')
42
+ }
43
+ return
44
+ }
45
+ }
46
+ }