@barefootjs/client 0.13.0 → 0.15.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/builtins.d.ts +44 -0
- package/dist/builtins.d.ts.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -2
- package/dist/reactive.d.ts +27 -0
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +122 -0
- package/dist/runtime/hydrate.d.ts +29 -0
- package/dist/runtime/hydrate.d.ts.map +1 -1
- package/dist/runtime/index.d.ts +2 -2
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +73 -8
- package/dist/runtime/standalone.js +280 -183
- package/dist/runtime/streaming.d.ts +5 -1
- package/dist/runtime/streaming.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/builtins.ts +63 -0
- package/src/index.ts +16 -0
- package/src/reactive.ts +130 -0
- package/src/runtime/hydrate.ts +113 -1
- package/src/runtime/index.ts +8 -1
- package/src/runtime/streaming.ts +18 -4
package/src/reactive.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* Inspired by SolidJS signals.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { BF_SEAM_PUSH_SEARCH } from '@barefootjs/shared'
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
11
|
* Phantom brand for compile-time reactivity detection.
|
|
10
12
|
* The compiler checks for the '__reactive' property via TypeChecker
|
|
@@ -597,3 +599,131 @@ export function createMemo<T>(fn: () => T, __bfId?: string): Memo<T> {
|
|
|
597
599
|
return value
|
|
598
600
|
}
|
|
599
601
|
|
|
602
|
+
|
|
603
|
+
// ---------------------------------------------------------------------------
|
|
604
|
+
// Request-scoped environment signals (router v0.5, spec/router.md "The wedge")
|
|
605
|
+
// ---------------------------------------------------------------------------
|
|
606
|
+
//
|
|
607
|
+
// An environment signal is ambient request/browser state — the query string,
|
|
608
|
+
// later cookies — that is correct per-request under SSR (read from the adapter's
|
|
609
|
+
// per-request context, never a process-wide module global, which would race) and
|
|
610
|
+
// reactive on the client (a query-only navigation updates it with no swap and no
|
|
611
|
+
// re-hydration; islands reconcile fine-grained). It rides the `Reactive<>` brand,
|
|
612
|
+
// so the compiler's reactivity analysis wires DOM updates with no new feature.
|
|
613
|
+
//
|
|
614
|
+
// These live HERE, in the single physical `@barefootjs/client/reactive` module,
|
|
615
|
+
// for the same reason the signal primitives do: both `@barefootjs/client` and
|
|
616
|
+
// the `/runtime` entry re-export them from this one module, so a page has ONE
|
|
617
|
+
// `searchParams` signal instance regardless of which entry an island imports
|
|
618
|
+
// from. A relative copy bundled into each entry would create two disconnected
|
|
619
|
+
// signals — the #1910 failure (the router would push into one while an island
|
|
620
|
+
// reads the other).
|
|
621
|
+
//
|
|
622
|
+
// No import-time side effect: the underlying signal is created lazily on first
|
|
623
|
+
// read and the router push seam is installed there (not at module top-level), so
|
|
624
|
+
// reading is the only thing that materialises anything.
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* SSR reader for request-scoped environment signals, keyed by env id
|
|
628
|
+
* (`'search'` today; `'cookie'` etc. later). Injected by an adapter / host so
|
|
629
|
+
* each value resolves per-request inside the host's async context — no shared
|
|
630
|
+
* mutable server state. ONE keyed reader serves every env signal, so a new
|
|
631
|
+
* signal needs no new seam or setter.
|
|
632
|
+
*/
|
|
633
|
+
let serverEnvReader: ((key: string) => string | undefined) | null = null
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Adapter/host hook: teach `@barefootjs/client` how to read the current
|
|
637
|
+
* request's environment values during SSR. The reader receives an env key
|
|
638
|
+
* (`'search'`, …) and returns the raw value, or `undefined` to defer (to the
|
|
639
|
+
* `globalThis` seam, else the empty default). Call once with a reader that
|
|
640
|
+
* resolves per-request.
|
|
641
|
+
*/
|
|
642
|
+
export function __bfSetServerEnvReader(
|
|
643
|
+
reader: ((key: string) => string | undefined) | null,
|
|
644
|
+
): void {
|
|
645
|
+
serverEnvReader = reader
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Resolve a request-scoped env value during SSR: the reader set via
|
|
650
|
+
* {@link __bfSetServerEnvReader}, else a `globalThis.__bf_serverEnvReader` seam
|
|
651
|
+
* — so a host can wire request-scoped SSR *without* importing
|
|
652
|
+
* `@barefootjs/client` (the server-side analogue of the `window.__bf_*` client
|
|
653
|
+
* seams). `undefined` when no reader resolves the key.
|
|
654
|
+
*/
|
|
655
|
+
function resolveServerEnv(key: string): string | undefined {
|
|
656
|
+
if (serverEnvReader) {
|
|
657
|
+
const v = serverEnvReader(key)
|
|
658
|
+
if (v !== undefined) return v
|
|
659
|
+
}
|
|
660
|
+
const seam = (
|
|
661
|
+
globalThis as unknown as { __bf_serverEnvReader?: (key: string) => string | undefined }
|
|
662
|
+
).__bf_serverEnvReader
|
|
663
|
+
return typeof seam === 'function' ? seam(key) : undefined
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Build a request-scoped reactive environment signal, keyed by `key` — the env
|
|
668
|
+
* id the SSR reader resolves (`'search'`, …). Internal: only concrete instances
|
|
669
|
+
* (`searchParams`, …) are exported.
|
|
670
|
+
*/
|
|
671
|
+
function createEnvSignal<T>(
|
|
672
|
+
key: string,
|
|
673
|
+
readClient: () => string,
|
|
674
|
+
parse: (raw: string) => T,
|
|
675
|
+
pushSeam: string,
|
|
676
|
+
): Reactive<() => T> {
|
|
677
|
+
let getRaw: (() => string) | null = null
|
|
678
|
+
|
|
679
|
+
function ensureClientSignal(): string {
|
|
680
|
+
if (!getRaw) {
|
|
681
|
+
const [get, set] = createSignal(readClient())
|
|
682
|
+
getRaw = get
|
|
683
|
+
// Install the router push seam inside the lazily-invoked accessor (not at
|
|
684
|
+
// module top-level), so reading is the only thing with an effect.
|
|
685
|
+
const w = window as unknown as Record<string, (next: string) => void>
|
|
686
|
+
// `set` already bails on `Object.is` equality, so no equality guard is
|
|
687
|
+
// needed — and a `get()` here would register a spurious dependency if a
|
|
688
|
+
// caller ever pushed from inside an effect.
|
|
689
|
+
w[pushSeam] = (next: string) => {
|
|
690
|
+
set(next)
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return getRaw()
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
return (() => {
|
|
697
|
+
if (typeof window === 'undefined') {
|
|
698
|
+
// SSR: resolve per-call inside the host's request context. Never cache a
|
|
699
|
+
// module-level signal — it would be a process-wide global that races.
|
|
700
|
+
return parse(resolveServerEnv(key) ?? '')
|
|
701
|
+
}
|
|
702
|
+
return parse(ensureClientSignal())
|
|
703
|
+
}) as Reactive<() => T>
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Reactive read of the current query string as `URLSearchParams`.
|
|
708
|
+
*
|
|
709
|
+
* ```tsx
|
|
710
|
+
* const sort = createMemo(() => searchParams().get('sort') ?? 'recent')
|
|
711
|
+
* ```
|
|
712
|
+
*
|
|
713
|
+
* A same-route, query-only navigation (`/list?sort=price`) driven by
|
|
714
|
+
* `@barefootjs/router` updates this signal and the URL **without a swap or
|
|
715
|
+
* re-hydration**. On the server it reflects the current request's query.
|
|
716
|
+
*
|
|
717
|
+
* Reactivity is **router-driven**: the signal is seeded once on first read and
|
|
718
|
+
* thereafter updated only through the `window.__bf_pushSearch` seam, which
|
|
719
|
+
* `startRouter()` drives (including on `popstate`). Without the router running
|
|
720
|
+
* — or after a non-router `history` change — the value is read-once; the seam
|
|
721
|
+
* name is shared with `@barefootjs/router` via `BF_SEAM_PUSH_SEARCH`, so the
|
|
722
|
+
* installer (here) and the pusher agree by construction.
|
|
723
|
+
*/
|
|
724
|
+
export const searchParams = createEnvSignal(
|
|
725
|
+
'search',
|
|
726
|
+
() => window.location.search,
|
|
727
|
+
(raw) => new URLSearchParams(raw),
|
|
728
|
+
BF_SEAM_PUSH_SEARCH,
|
|
729
|
+
)
|
package/src/runtime/hydrate.ts
CHANGED
|
@@ -43,6 +43,7 @@ import { hydratedScopes } from './hydration-state.ts'
|
|
|
43
43
|
import { registerComponent } from './registry.ts'
|
|
44
44
|
import { registerTemplate } from './template.ts'
|
|
45
45
|
import { BF_SCOPE, BF_PROPS, BF_HOST, BF_SCOPE_COMMENT_PREFIX } from '@barefootjs/shared'
|
|
46
|
+
import { createRoot } from '@barefootjs/client/reactive'
|
|
46
47
|
import type { ComponentDef } from './types.ts'
|
|
47
48
|
|
|
48
49
|
/**
|
|
@@ -185,6 +186,98 @@ export function flushHydration(): void {
|
|
|
185
186
|
walkAllInDocumentOrder()
|
|
186
187
|
}
|
|
187
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Re-hydrate only the scopes inside `root` (and `root` itself), in
|
|
191
|
+
* document order. Unlike `rehydrateAll()` — which schedules a walk over
|
|
192
|
+
* the *whole document* — this is a synchronous, subtree-scoped walk: its
|
|
193
|
+
* cost is O(scopes in `root`), not O(document).
|
|
194
|
+
*
|
|
195
|
+
* Intended for a client router that has just swapped a content region:
|
|
196
|
+
* the component registry is already populated from the initial load, so
|
|
197
|
+
* no deferral is needed; only the freshly inserted islands need to init.
|
|
198
|
+
*/
|
|
199
|
+
export function rehydrateScope(root: Element): void {
|
|
200
|
+
if (typeof document === 'undefined') return
|
|
201
|
+
// The TreeWalker visits descendants only, so handle the root itself first.
|
|
202
|
+
hydrateElementScope(root)
|
|
203
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT)
|
|
204
|
+
while (walker.nextNode()) {
|
|
205
|
+
const node = walker.currentNode
|
|
206
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
207
|
+
hydrateElementScope(node as Element)
|
|
208
|
+
} else if (node.nodeType === Node.COMMENT_NODE) {
|
|
209
|
+
hydrateCommentScope(node as Comment)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Dispose every hydrated scope inside `root` (and `root` itself): runs
|
|
216
|
+
* each scope's `createRoot` dispose fn, tearing down its effects, memos,
|
|
217
|
+
* and `onCleanup` callbacks. Used by a client router before it removes a
|
|
218
|
+
* content region, so the outgoing islands release timers/listeners/
|
|
219
|
+
* subscriptions instead of leaking.
|
|
220
|
+
*
|
|
221
|
+
* Also resets the per-scope hydration marks so the *same* DOM nodes could
|
|
222
|
+
* be re-hydrated if they are ever re-inserted:
|
|
223
|
+
* - element scopes: their `hydratedScopes` entry (via `disposeOneScope`),
|
|
224
|
+
* - `bf-h` child scopes: their `hydratedScopes` entry (they have no own
|
|
225
|
+
* disposer — their reactive graph is owned by an ancestor root being
|
|
226
|
+
* disposed here),
|
|
227
|
+
* - comment scopes: the `__bfInitialized` flag on the `<!--bf-scope:-->`
|
|
228
|
+
* anchor (otherwise `hydrateCommentScope` short-circuits on the stale
|
|
229
|
+
* flag and never re-initializes the scope).
|
|
230
|
+
*/
|
|
231
|
+
export function disposeScope(root: Element): void {
|
|
232
|
+
if (typeof document === 'undefined') return
|
|
233
|
+
disposeOneScope(root)
|
|
234
|
+
clearChildScopeMark(root)
|
|
235
|
+
const walker = document.createTreeWalker(
|
|
236
|
+
root,
|
|
237
|
+
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT,
|
|
238
|
+
)
|
|
239
|
+
while (walker.nextNode()) {
|
|
240
|
+
const node = walker.currentNode
|
|
241
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
242
|
+
disposeOneScope(node as Element)
|
|
243
|
+
clearChildScopeMark(node as Element)
|
|
244
|
+
} else if (node.nodeType === Node.COMMENT_NODE) {
|
|
245
|
+
clearCommentScopeFlag(node as Comment)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function disposeOneScope(el: Element): void {
|
|
251
|
+
const dispose = scopeDisposers.get(el)
|
|
252
|
+
if (dispose) {
|
|
253
|
+
scopeDisposers.delete(el)
|
|
254
|
+
hydratedScopes.delete(el)
|
|
255
|
+
dispose()
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* `bf-h` child scopes are initialized via `initChild` (not the walker), so
|
|
261
|
+
* they carry a `hydratedScopes` mark but no own disposer — their effects
|
|
262
|
+
* belong to an ancestor root that `disposeScope` is tearing down. Clear the
|
|
263
|
+
* mark so the same node can re-init if re-inserted.
|
|
264
|
+
*/
|
|
265
|
+
function clearChildScopeMark(el: Element): void {
|
|
266
|
+
if (el.hasAttribute(BF_HOST)) hydratedScopes.delete(el)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Comment-rooted scopes flag their init on the `<!--bf-scope:-->` comment
|
|
271
|
+
* node (`__bfInitialized`); the proxy element's `hydratedScopes` entry and
|
|
272
|
+
* disposer are cleared by `disposeOneScope`, but the comment flag must be
|
|
273
|
+
* reset here or re-hydration short-circuits.
|
|
274
|
+
*/
|
|
275
|
+
function clearCommentScopeFlag(comment: Comment): void {
|
|
276
|
+
if (!comment.nodeValue?.startsWith(BF_SCOPE_COMMENT_PREFIX)) return
|
|
277
|
+
const flagged = comment as unknown as { __bfInitialized?: boolean }
|
|
278
|
+
if (flagged.__bfInitialized) flagged.__bfInitialized = false
|
|
279
|
+
}
|
|
280
|
+
|
|
188
281
|
/**
|
|
189
282
|
* Single document-order walk visiting element scopes (`[bf-s]`) and
|
|
190
283
|
* comment scopes (`<!--bf-scope:Name_xxx-->`) interleaved by their
|
|
@@ -232,10 +325,29 @@ function parseProps(json: string | null, where: string): Record<string, unknown>
|
|
|
232
325
|
}
|
|
233
326
|
}
|
|
234
327
|
|
|
328
|
+
/**
|
|
329
|
+
* Per-scope disposal registry. Each scope's `init` runs inside a
|
|
330
|
+
* `createRoot`, so all the effects/memos it (and its `initChild`-mounted
|
|
331
|
+
* descendants) create are owned by one root. `disposeScope(root)` calls
|
|
332
|
+
* the stored dispose fn to tear them down — enabling a client router to
|
|
333
|
+
* release the islands leaving the page precisely, instead of leaking
|
|
334
|
+
* timers/listeners or relying on GC (see `disposeScope`).
|
|
335
|
+
*
|
|
336
|
+
* Keyed by the scope element; a `WeakMap` so detached scopes are
|
|
337
|
+
* reclaimed even if `disposeScope` is never called.
|
|
338
|
+
*/
|
|
339
|
+
const scopeDisposers = new WeakMap<Element, () => void>()
|
|
340
|
+
|
|
235
341
|
function runInit(scope: Element, def: ComponentDef, props: Record<string, unknown>): void {
|
|
236
342
|
const prevScope = setCurrentScope(scope)
|
|
237
343
|
try {
|
|
238
|
-
|
|
344
|
+
// Wrap in createRoot so the scope's reactive graph has an owner that
|
|
345
|
+
// can be disposed later. This is additive: nothing disposes the root
|
|
346
|
+
// unless `disposeScope` is called, so existing lifetimes are unchanged.
|
|
347
|
+
createRoot((dispose) => {
|
|
348
|
+
scopeDisposers.set(scope, dispose)
|
|
349
|
+
def.init(scope, props)
|
|
350
|
+
})
|
|
239
351
|
} finally {
|
|
240
352
|
setCurrentScope(prevScope)
|
|
241
353
|
}
|
package/src/runtime/index.ts
CHANGED
|
@@ -22,6 +22,13 @@ export {
|
|
|
22
22
|
beginTurn,
|
|
23
23
|
endTurn,
|
|
24
24
|
__bfReportOutput,
|
|
25
|
+
// Request-scoped env signal (router v0.5). The compiler emits island client JS
|
|
26
|
+
// that imports `searchParams` from `@barefootjs/client/runtime`; re-exporting
|
|
27
|
+
// it from the shared reactive module (same as the signal primitives above)
|
|
28
|
+
// means this entry and the main `@barefootjs/client` entry resolve to ONE
|
|
29
|
+
// signal instance — no second copy to disconnect from router pushes.
|
|
30
|
+
searchParams,
|
|
31
|
+
__bfSetServerEnvReader,
|
|
25
32
|
type Reactive,
|
|
26
33
|
type Signal,
|
|
27
34
|
type Memo,
|
|
@@ -92,7 +99,7 @@ export { styleToCss } from './style.ts'
|
|
|
92
99
|
|
|
93
100
|
// Runtime helpers
|
|
94
101
|
export { findScope, find, $, $c, $t, qsa, qsaChildScope, qsaChildScopes, cssEscape } from './query.ts'
|
|
95
|
-
export { hydrate, rehydrateAll, flushHydration, getRegisteredDef } from './hydrate.ts'
|
|
102
|
+
export { hydrate, rehydrateAll, rehydrateScope, disposeScope, flushHydration, getRegisteredDef } from './hydrate.ts'
|
|
96
103
|
export { registerComponent, getComponentInit, initChild, upsertChild } from './registry.ts'
|
|
97
104
|
export { insert, type BranchConfig, type BranchTemplateResult } from './insert.ts'
|
|
98
105
|
export { __bfSlot } from './branch-slot.ts'
|
package/src/runtime/streaming.ts
CHANGED
|
@@ -13,8 +13,14 @@
|
|
|
13
13
|
* 3. This module swaps fallback → resolved content and triggers hydration.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import {
|
|
17
|
-
|
|
16
|
+
import {
|
|
17
|
+
BF_ASYNC,
|
|
18
|
+
BF_ASYNC_RESOLVE,
|
|
19
|
+
BF_SEAM_DISPOSE_WITHIN,
|
|
20
|
+
BF_SEAM_HYDRATE,
|
|
21
|
+
BF_SEAM_HYDRATE_WITHIN,
|
|
22
|
+
} from '@barefootjs/shared'
|
|
23
|
+
import { rehydrateAll, rehydrateScope, disposeScope } from './hydrate.ts'
|
|
18
24
|
|
|
19
25
|
/**
|
|
20
26
|
* Swap a streaming fallback placeholder with its resolved content.
|
|
@@ -52,7 +58,11 @@ export function __bf_swap(id: string): void {
|
|
|
52
58
|
* Makes `__bf_swap` available as `window.__bf_swap` so that inline
|
|
53
59
|
* `<script>__bf_swap("a0")</script>` tags in streaming chunks can call it.
|
|
54
60
|
*
|
|
55
|
-
* Also exposes
|
|
61
|
+
* Also exposes re-hydration / disposal seams for an out-of-tree client
|
|
62
|
+
* router (`@barefootjs/router`):
|
|
63
|
+
* - `window.__bf_hydrate` — re-hydrate the whole document
|
|
64
|
+
* - `window.__bf_hydrate_within` — re-hydrate only a swapped subtree
|
|
65
|
+
* - `window.__bf_dispose_within` — dispose the islands in a subtree
|
|
56
66
|
*
|
|
57
67
|
* Call this once, early in the page (before any streaming chunks arrive).
|
|
58
68
|
*/
|
|
@@ -61,5 +71,9 @@ export function setupStreaming(): void {
|
|
|
61
71
|
|
|
62
72
|
const w = window as unknown as Record<string, unknown>
|
|
63
73
|
w.__bf_swap = __bf_swap
|
|
64
|
-
|
|
74
|
+
// Seam names shared with `@barefootjs/router` via `@barefootjs/shared` so the
|
|
75
|
+
// installer (here) and the reader (the router) can never drift apart.
|
|
76
|
+
w[BF_SEAM_HYDRATE] = rehydrateAll
|
|
77
|
+
w[BF_SEAM_HYDRATE_WITHIN] = rehydrateScope
|
|
78
|
+
w[BF_SEAM_DISPOSE_WITHIN] = disposeScope
|
|
65
79
|
}
|