@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,311 @@
1
+ /**
2
+ * BarefootJS - Hydration
3
+ *
4
+ * Combined component registration + template registration + hydration.
5
+ * Single entry point for compiler-generated code.
6
+ *
7
+ * Design (post #1172):
8
+ *
9
+ * 1. `hydrate(name, def)` only **registers** the def + template +
10
+ * component-registry entry, then schedules a walk.
11
+ * 2. The walk visits every scope in **true document order** — both
12
+ * `[bf-s]` element scopes and `<!--bf-scope:Name_xxx-->` comment
13
+ * scopes are interleaved by their actual DOM position. Parents
14
+ * always init before descendants regardless of which kind of
15
+ * scope each one is, so a comment-rooted parent that calls
16
+ * `provideContext()` is visible to an element-rooted descendant
17
+ * that calls `useContext()` on the very first hydrate pass.
18
+ * 3. Two scheduling phases, each capped to one in-flight callback:
19
+ * - microtask: collapses every `hydrate()` call from the
20
+ * bundled module body into a single walk on the next tick.
21
+ * - rAF: catches scope elements that the streaming protocol
22
+ * (Hono / `__bf_swap`) injects into the document *after*
23
+ * the synchronous module body has finished.
24
+ * 4. `rehydrateAll()` and the comment-scope path both share this
25
+ * walker, so streaming swaps and comment-rooted fragments enjoy
26
+ * the same ordering guarantee.
27
+ *
28
+ * Same-name nesting (`<Counter>` inside `<Counter>`):
29
+ * The walker's `hydratedScopes.has(el)` check is the *only* skip
30
+ * guard. Parents that intentionally own their nested same-name
31
+ * children call `initChild(...)` from their init body — initChild
32
+ * marks the child scope as hydrated, so when the walker reaches it
33
+ * later it short-circuits. Parents that *don't* call `initChild`
34
+ * (the descendant is a coincidental same-name component, not a
35
+ * structural child) get the descendant hydrated by the walker as a
36
+ * normal top-level component. This makes nesting depth a non-issue
37
+ * — the previous ancestor-walk guard is gone.
38
+ */
39
+
40
+ import { setCurrentScope } from './context'
41
+ import { commentScopeRegistry } from './scope'
42
+ import { hydratedScopes } from './hydration-state'
43
+ import { registerComponent } from './registry'
44
+ import { registerTemplate } from './template'
45
+ import { BF_SCOPE, BF_PROPS, BF_HOST, BF_SCOPE_COMMENT_PREFIX } from '@barefootjs/shared'
46
+ import type { ComponentDef } from './types'
47
+
48
+ /**
49
+ * Registry of all hydrated component definitions.
50
+ * Used by the walker to look up an element's init/def by name, and by
51
+ * rehydrateAll() to re-scan the DOM after streaming chunks arrive.
52
+ */
53
+ const registeredDefs = new Map<string, ComponentDef>()
54
+
55
+ /**
56
+ * Look up a registered component definition by name. Used by the CSR
57
+ * `createComponent` path to honour `comment: true` (transparent
58
+ * wrapper) components — without the def lookup, those components
59
+ * overwrite the inner element's `bf-s` and break child-scope
60
+ * resolution at hydration.
61
+ */
62
+ export function getRegisteredDef(name: string): ComponentDef | undefined {
63
+ return registeredDefs.get(name)
64
+ }
65
+
66
+ let microtaskScheduled = false
67
+ let rafScheduled = false
68
+
69
+ /**
70
+ * Cross-runtime microtask scheduler. `queueMicrotask` is widely
71
+ * supported but absent in some test DOMs / older runtimes; fall back
72
+ * to `Promise.resolve().then(...)` so importing this module never
73
+ * throws on environments missing the global.
74
+ */
75
+ const scheduleMicrotask: (cb: () => void) => void =
76
+ typeof queueMicrotask === 'function'
77
+ ? queueMicrotask
78
+ : (cb) => {
79
+ Promise.resolve().then(cb)
80
+ }
81
+
82
+ /**
83
+ * Schedule the document-order walk once per tick (microtask) and once
84
+ * per frame (rAF). Both flags are cleared inside their respective
85
+ * callbacks, so a flood of `hydrate()` / `rehydrateAll()` calls can
86
+ * never queue more than two pending walks in total. The callbacks
87
+ * also re-check their own flag on entry so that a synchronous
88
+ * `flushHydration()` between scheduling and firing turns the queued
89
+ * callback into a no-op.
90
+ */
91
+ function scheduleWalk(): void {
92
+ if (!microtaskScheduled) {
93
+ microtaskScheduled = true
94
+ scheduleMicrotask(() => {
95
+ if (!microtaskScheduled) return
96
+ microtaskScheduled = false
97
+ walkAllInDocumentOrder()
98
+ })
99
+ }
100
+ if (!rafScheduled && typeof requestAnimationFrame === 'function') {
101
+ rafScheduled = true
102
+ requestAnimationFrame(() => {
103
+ if (!rafScheduled) return
104
+ rafScheduled = false
105
+ walkAllInDocumentOrder()
106
+ })
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Register a component and schedule a document-order hydration walk.
112
+ * Combines registration + template setup + hydration in a single call.
113
+ *
114
+ * **Scheduling semantics** (changed in #1172): the walk runs on the
115
+ * next microtask, then again on the next animation frame. The init
116
+ * functions for registered components are *not* invoked synchronously
117
+ * inside `hydrate()`. Code that needs to observe init effects on the
118
+ * same tick — typically tests, but also advanced consumers wiring
119
+ * imperative bridges — should either:
120
+ *
121
+ * - `await Promise.resolve()` after a batch of `hydrate()` calls, or
122
+ * - call `flushHydration()` (see below) to drain any pending walks
123
+ * synchronously.
124
+ *
125
+ * The deferral is what lets the doc-order walker see a fully populated
126
+ * registry: every `hydrate()` call from the bundled module body lands
127
+ * in the registry *before* the microtask flush kicks off the single
128
+ * walk, so parents always init before their descendants regardless of
129
+ * which file the parent's `hydrate()` was emitted into.
130
+ *
131
+ * @param name - Component name
132
+ * @param def - Component definition (init function + optional template + comment flag)
133
+ */
134
+ export function hydrate(name: string, def: ComponentDef): void {
135
+ // Ensure name is always set on the def so createComponentFromDef()
136
+ // doesn't rely on def.init.name (which may be lost under minification).
137
+ def.name = name
138
+
139
+ registeredDefs.set(name, def)
140
+
141
+ // Register component for parent-child communication
142
+ registerComponent(name, def.init)
143
+
144
+ // Register template for client-side component creation
145
+ if (def.template) {
146
+ registerTemplate(name, def.template)
147
+ }
148
+
149
+ scheduleWalk()
150
+ }
151
+
152
+ /**
153
+ * Re-hydrate all registered components.
154
+ *
155
+ * Called by the streaming resolver after swapping fallback content with
156
+ * resolved content. Goes through the same scheduler as `hydrate()` so
157
+ * back-to-back `__bf_swap` invocations or interleaved `hydrate()` calls
158
+ * collapse to a single walk per tick + per frame. Like `hydrate()` this
159
+ * is asynchronous — see `flushHydration()` if you need a synchronous
160
+ * drain.
161
+ */
162
+ export function rehydrateAll(): void {
163
+ scheduleWalk()
164
+ }
165
+
166
+ /**
167
+ * Run any pending hydration walk synchronously, right now.
168
+ *
169
+ * The default scheduler is microtask + rAF — the right trade-off for
170
+ * production code (registry populates before the walk; back-to-back
171
+ * `hydrate()` calls coalesce). Tests and advanced consumers that need
172
+ * a deterministic completion point — e.g. imperative mounting code
173
+ * reading DOM state immediately after `render(...)` — call this
174
+ * helper instead of awaiting a microtask.
175
+ *
176
+ * @example
177
+ * hydrate('Counter', def)
178
+ * flushHydration()
179
+ * // safe to read Counter's post-init DOM state
180
+ */
181
+ export function flushHydration(): void {
182
+ if (!microtaskScheduled && !rafScheduled) return
183
+ microtaskScheduled = false
184
+ rafScheduled = false
185
+ walkAllInDocumentOrder()
186
+ }
187
+
188
+ /**
189
+ * Single document-order walk visiting element scopes (`[bf-s]`) and
190
+ * comment scopes (`<!--bf-scope:Name_xxx-->`) interleaved by their
191
+ * actual DOM position. The parent's init — whichever scope shape it
192
+ * takes — always runs before any descendant init, so a comment-scope
193
+ * provider is visible to an element-scope descendant on the first pass.
194
+ *
195
+ * Both paths skip `~`-prefixed scopes (owned by `initChild`) and mark
196
+ * the scope as hydrated *before* running init: re-entrant `hydrate()`
197
+ * / `rehydrateAll()` calls from the init body (or a synchronous effect
198
+ * they trigger) must see the slot already taken so the next scheduled
199
+ * walk doesn't re-enter the same scope.
200
+ */
201
+ function walkAllInDocumentOrder(): void {
202
+ if (typeof document === 'undefined') return
203
+
204
+ const walker = document.createTreeWalker(
205
+ document,
206
+ NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT,
207
+ )
208
+
209
+ while (walker.nextNode()) {
210
+ const node = walker.currentNode
211
+ if (node.nodeType === Node.ELEMENT_NODE) {
212
+ hydrateElementScope(node as Element)
213
+ } else if (node.nodeType === Node.COMMENT_NODE) {
214
+ hydrateCommentScope(node as Comment)
215
+ }
216
+ }
217
+ }
218
+
219
+ /** Component name segment of a scope ID (everything before the first `_`). */
220
+ function scopeName(id: string): string | null {
221
+ const idx = id.indexOf('_')
222
+ return idx < 0 ? null : id.slice(0, idx)
223
+ }
224
+
225
+ function parseProps(json: string | null, where: string): Record<string, unknown> {
226
+ if (!json) return {}
227
+ try {
228
+ return JSON.parse(json) as Record<string, unknown>
229
+ } catch {
230
+ console.warn(`[BarefootJS] Invalid props JSON on ${where}:`, json)
231
+ return {}
232
+ }
233
+ }
234
+
235
+ function runInit(scope: Element, def: ComponentDef, props: Record<string, unknown>): void {
236
+ const prevScope = setCurrentScope(scope)
237
+ try {
238
+ def.init(scope, props)
239
+ } finally {
240
+ setCurrentScope(prevScope)
241
+ }
242
+ }
243
+
244
+ function hydrateElementScope(el: Element): void {
245
+ if (hydratedScopes.has(el)) return
246
+
247
+ const bfs = el.getAttribute(BF_SCOPE)
248
+ if (!bfs) return
249
+ // Skip child scopes — parent's initChild call owns their lifecycle.
250
+ // Child detection uses bf-h presence (#1249).
251
+ if (el.hasAttribute(BF_HOST)) return
252
+
253
+ const name = scopeName(bfs)
254
+ if (!name) return
255
+
256
+ const def = registeredDefs.get(name)
257
+ if (!def) return
258
+
259
+ hydratedScopes.add(el)
260
+ // Comment-based components hydrate via the comment-scope path; their
261
+ // bf-s attribute sits on a proxy element. Marking it above lets the
262
+ // next walk skip at the WeakSet check rather than re-resolving the def.
263
+ if (def.comment) return
264
+
265
+ runInit(el, def, parseProps(el.getAttribute(BF_PROPS), bfs))
266
+ }
267
+
268
+ function hydrateCommentScope(comment: Comment): void {
269
+ const value = comment.nodeValue
270
+ if (!value?.startsWith(BF_SCOPE_COMMENT_PREFIX)) return
271
+
272
+ const rest = value.slice(BF_SCOPE_COMMENT_PREFIX.length)
273
+ // Comment-scope child detection: child fragment scopes embed host metadata
274
+ // as a `|h=<hostBfs>|m=<slot>` segment after the scope id. Their parent's
275
+ // initChild call owns hydration; the walker must skip them.
276
+ // Root scopes have only `|<propsJson>` (no `h=` segment).
277
+ if (rest.includes('|h=')) return
278
+
279
+ const flagged = comment as unknown as { __bfInitialized?: boolean }
280
+ if (flagged.__bfInitialized) return
281
+
282
+ const pipeIdx = rest.indexOf('|')
283
+ const scopeId = pipeIdx >= 0 ? rest.slice(0, pipeIdx) : rest
284
+ const propsJson = pipeIdx >= 0 ? rest.slice(pipeIdx + 1) : ''
285
+
286
+ const name = scopeName(scopeId)
287
+ if (!name) return
288
+
289
+ const def = registeredDefs.get(name)
290
+ if (!def?.comment) return
291
+
292
+ flagged.__bfInitialized = true
293
+
294
+ const proxyEl = nextElementSibling(comment) ?? comment.parentElement
295
+ if (!proxyEl) return
296
+
297
+ commentScopeRegistry.set(proxyEl, { commentNode: comment, scopeId })
298
+
299
+ const parsed = parseProps(propsJson || null, `comment scope ${scopeId}`)
300
+ const props = (parsed[name] ?? {}) as Record<string, unknown>
301
+ runInit(proxyEl, def, props)
302
+ }
303
+
304
+ function nextElementSibling(node: Node): Element | null {
305
+ let sibling: Node | null = node.nextSibling
306
+ while (sibling) {
307
+ if (sibling.nodeType === Node.ELEMENT_NODE) return sibling as Element
308
+ sibling = sibling.nextSibling
309
+ }
310
+ return null
311
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * BarefootJS - Hydration State
3
+ *
4
+ * Tracks which scope elements have been hydrated using a WeakSet
5
+ * instead of a DOM attribute. This avoids polluting the DOM and
6
+ * enables tree-shaking.
7
+ */
8
+
9
+ /**
10
+ * Set of scope elements that have been initialized/hydrated.
11
+ * Used to prevent duplicate initialization.
12
+ */
13
+ export const hydratedScopes = new WeakSet<Element>()
@@ -0,0 +1,96 @@
1
+ // Re-export all user-facing @barefootjs/client APIs so compiler-generated
2
+ // code can use a single import source.
3
+ //
4
+ // The reactive runtime has module-local state (`Listener`, `Owner`, the
5
+ // pending-effect queue) and MUST NOT be duplicated across bundles —
6
+ // otherwise a signal created via one copy is invisible to an effect
7
+ // registered via the other. Both `@barefootjs/client` (main) and this
8
+ // `/runtime` entry pull the reactive primitives from the shared
9
+ // `@barefootjs/client/reactive` subpath so downstream bundlers see a
10
+ // single physical module.
11
+ export {
12
+ createSignal,
13
+ createEffect,
14
+ createDisposableEffect,
15
+ createMemo,
16
+ createRoot,
17
+ onCleanup,
18
+ onMount,
19
+ untrack,
20
+ batch,
21
+ type Reactive,
22
+ type Signal,
23
+ type Memo,
24
+ type CleanupFn,
25
+ type EffectFn,
26
+ } from '@barefootjs/client/reactive'
27
+
28
+ export { splitProps } from '../split-props'
29
+ export { __slot, type SlotMarker } from '../slot'
30
+ export { forwardProps } from '../forward-props'
31
+ export { unwrap } from '../unwrap'
32
+
33
+ // Context API (real DOM-bound implementations; `createContext` is the
34
+ // same pure function re-exported from `../context`).
35
+ export {
36
+ createContext,
37
+ useContext,
38
+ provideContext,
39
+ setCurrentScope,
40
+ type Context,
41
+ } from './context'
42
+
43
+ // Portal system
44
+ export {
45
+ createPortal,
46
+ isSSRPortal,
47
+ findSiblingSlot,
48
+ cleanupPortalPlaceholder,
49
+ type Portal,
50
+ type PortalOptions,
51
+ type Renderable,
52
+ type PortalChildren,
53
+ } from './portal'
54
+
55
+ // List reconciliation
56
+ export { reconcileList, type RenderItemFn } from './list'
57
+ export { reconcileElements, getLoopChildren, getLoopNodes } from './reconcile-elements'
58
+ export { qsaItem, upsertChildItem } from './qsa-item'
59
+ export { mapArray } from './map-array'
60
+
61
+ // Template registry
62
+ export { registerTemplate, getTemplate, hasTemplate, type TemplateFn } from './template'
63
+
64
+ // Component creation
65
+ export {
66
+ createComponent,
67
+ renderChild,
68
+ getPropsUpdateFn,
69
+ getComponentProps,
70
+ parseHTML,
71
+ } from './component'
72
+
73
+ // Spread props helpers
74
+ export { applyRestAttrs } from './apply-rest-attrs'
75
+ export { spreadAttrs } from './spread-attrs'
76
+ export { styleToCss } from './style'
77
+
78
+ // Runtime helpers
79
+ export { findScope, find, $, $c, $t, qsa, qsaChildScope, qsaChildScopes, cssEscape } from './query'
80
+ export { hydrate, rehydrateAll, flushHydration, getRegisteredDef } from './hydrate'
81
+ export { registerComponent, getComponentInit, initChild, upsertChild } from './registry'
82
+ export { insert, type BranchConfig, type BranchTemplateResult } from './insert'
83
+ export { __bfSlot } from './branch-slot'
84
+ export { updateClientMarker } from './client-marker'
85
+
86
+ // Hydration state
87
+ export { hydratedScopes } from './hydration-state'
88
+
89
+ // CSR entry point
90
+ export { render } from './render'
91
+
92
+ // Streaming (Out-of-Order SSR)
93
+ export { __bf_swap, setupStreaming } from './streaming'
94
+
95
+ // Core types
96
+ export type { InitFn, ComponentDef } from './types'