@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.
- package/dist/build.d.ts +56 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +76 -0
- package/dist/context.d.ts +25 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/csr-adapter.d.ts +26 -0
- package/dist/csr-adapter.d.ts.map +1 -0
- package/dist/forward-props.d.ts +17 -0
- package/dist/forward-props.d.ts.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +154 -0
- package/dist/reactive.d.ts +150 -0
- package/dist/reactive.d.ts.map +1 -0
- package/dist/reactive.js +215 -0
- package/dist/runtime/apply-rest-attrs.d.ts +16 -0
- package/dist/runtime/apply-rest-attrs.d.ts.map +1 -0
- package/dist/runtime/branch-slot.d.ts +22 -0
- package/dist/runtime/branch-slot.d.ts.map +1 -0
- package/dist/runtime/client-marker.d.ts +21 -0
- package/dist/runtime/client-marker.d.ts.map +1 -0
- package/dist/runtime/component.d.ts +99 -0
- package/dist/runtime/component.d.ts.map +1 -0
- package/dist/runtime/context.d.ts +40 -0
- package/dist/runtime/context.d.ts.map +1 -0
- package/dist/runtime/hydrate.d.ts +100 -0
- package/dist/runtime/hydrate.d.ts.map +1 -0
- package/dist/runtime/hydration-state.d.ts +13 -0
- package/dist/runtime/hydration-state.d.ts.map +1 -0
- package/dist/runtime/index.d.ts +27 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +2093 -0
- package/dist/runtime/insert.d.ts +75 -0
- package/dist/runtime/insert.d.ts.map +1 -0
- package/dist/runtime/list.d.ts +21 -0
- package/dist/runtime/list.d.ts.map +1 -0
- package/dist/runtime/map-array.d.ts +32 -0
- package/dist/runtime/map-array.d.ts.map +1 -0
- package/dist/runtime/portal.d.ts +96 -0
- package/dist/runtime/portal.d.ts.map +1 -0
- package/dist/runtime/qsa-item.d.ts +52 -0
- package/dist/runtime/qsa-item.d.ts.map +1 -0
- package/dist/runtime/query.d.ts +86 -0
- package/dist/runtime/query.d.ts.map +1 -0
- package/dist/runtime/reconcile-elements.d.ts +44 -0
- package/dist/runtime/reconcile-elements.d.ts.map +1 -0
- package/dist/runtime/registry.d.ts +53 -0
- package/dist/runtime/registry.d.ts.map +1 -0
- package/dist/runtime/render.d.ts +35 -0
- package/dist/runtime/render.d.ts.map +1 -0
- package/dist/runtime/scope.d.ts +28 -0
- package/dist/runtime/scope.d.ts.map +1 -0
- package/dist/runtime/slot-resolver.d.ts +36 -0
- package/dist/runtime/slot-resolver.d.ts.map +1 -0
- package/dist/runtime/spread-attrs.d.ts +19 -0
- package/dist/runtime/spread-attrs.d.ts.map +1 -0
- package/dist/runtime/standalone.js +2278 -0
- package/dist/runtime/streaming.d.ts +36 -0
- package/dist/runtime/streaming.d.ts.map +1 -0
- package/dist/runtime/style.d.ts +17 -0
- package/dist/runtime/style.d.ts.map +1 -0
- package/dist/runtime/template.d.ts +39 -0
- package/dist/runtime/template.d.ts.map +1 -0
- package/dist/runtime/types.d.ts +26 -0
- package/dist/runtime/types.d.ts.map +1 -0
- package/dist/shims.d.ts +21 -0
- package/dist/shims.d.ts.map +1 -0
- package/dist/slot.d.ts +14 -0
- package/dist/slot.d.ts.map +1 -0
- package/dist/split-props.d.ts +26 -0
- package/dist/split-props.d.ts.map +1 -0
- package/dist/unwrap.d.ts +16 -0
- package/dist/unwrap.d.ts.map +1 -0
- package/package.json +71 -0
- package/src/build.ts +92 -0
- package/src/context.ts +33 -0
- package/src/csr-adapter.ts +134 -0
- package/src/forward-props.ts +43 -0
- package/src/index.ts +42 -0
- package/src/reactive.ts +411 -0
- package/src/runtime/apply-rest-attrs.ts +109 -0
- package/src/runtime/branch-slot.ts +32 -0
- package/src/runtime/client-marker.ts +46 -0
- package/src/runtime/component.ts +501 -0
- package/src/runtime/context.ts +111 -0
- package/src/runtime/hydrate.ts +311 -0
- package/src/runtime/hydration-state.ts +13 -0
- package/src/runtime/index.ts +96 -0
- package/src/runtime/insert.ts +407 -0
- package/src/runtime/list.ts +47 -0
- package/src/runtime/map-array.ts +381 -0
- package/src/runtime/portal.ts +174 -0
- package/src/runtime/qsa-item.ts +128 -0
- package/src/runtime/query.ts +632 -0
- package/src/runtime/reconcile-elements.ts +391 -0
- package/src/runtime/registry.ts +160 -0
- package/src/runtime/render.ts +105 -0
- package/src/runtime/scope.ts +46 -0
- package/src/runtime/slot-resolver.ts +66 -0
- package/src/runtime/spread-attrs.ts +88 -0
- package/src/runtime/streaming.ts +65 -0
- package/src/runtime/style.ts +27 -0
- package/src/runtime/template.ts +53 -0
- package/src/runtime/types.ts +27 -0
- package/src/shims.ts +54 -0
- package/src/slot.ts +23 -0
- package/src/split-props.ts +86 -0
- package/src/unwrap.ts +18 -0
package/src/reactive.ts
ADDED
|
@@ -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
|
+
}
|