@brust/runtime-dom 2.0.0-alpha.20 → 2.0.0-alpha.22
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/package.json +1 -1
- package/src/core-api.ts +1 -1
- package/src/directives/for.ts +9 -3
- package/src/directives/index.ts +11 -1
- package/src/directives/ref.ts +2 -6
- package/src/instance.ts +11 -9
- package/src/registry.ts +1 -1
- package/src/signal.ts +14 -6
package/package.json
CHANGED
package/src/core-api.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// What a feature module may import from the core, and nothing else. In the browser build the core is one shared
|
|
2
2
|
// ESM file and every feature is a separate file that imports these names from it (bundle.ts maps this module to the
|
|
3
3
|
// core URL), so a feature registers on the page's single binder registry and signal graph.
|
|
4
|
-
export { BINDERS, bindHost, bindElement, walkChildren } from './directives/index'
|
|
4
|
+
export { BINDERS, bindHost, bindElement, bindRef, walkChildren } from './directives/index'
|
|
5
5
|
export type { Binder } from './directives/index'
|
|
6
6
|
export { parsed, read, member, mismatch, insideRaw, RAW_SEL } from './directives/common'
|
|
7
7
|
export { hooks, islandHooks, collectionHooks } from './instance'
|
package/src/directives/for.ts
CHANGED
|
@@ -4,6 +4,11 @@ type RowScope = Record<string, unknown> & { __own: Signal<number> }
|
|
|
4
4
|
|
|
5
5
|
const SYNTAX = /^\s*([A-Za-z_$][\w$]*)\s*(?:,\s*([A-Za-z_$][\w$]*))?\s+in\s+([A-Za-z_$][\w$.]*(?::[A-Za-z_$][\w$]*(?:\s*,\s*[A-Za-z_$][\w$]*)*)?)\s+by\s+([A-Za-z_$][\w$.]*)\s*$/
|
|
6
6
|
const done = new WeakSet<Element>()
|
|
7
|
+
/** The comments this binder inserted: the server's own `<!--x-for-->` marker is not among them. */
|
|
8
|
+
const anchors = new WeakSet<Node>()
|
|
9
|
+
/** F396: the zero-row template is the element the server prints right after its `<!--x-for-->` marker. A row is not
|
|
10
|
+
* told apart by `hidden`: a row whose own root is `hidden={cond}` is server-rendered hidden and is still a row. */
|
|
11
|
+
const isTemplate = (s: Element) => { const p = s.previousSibling; return p instanceof Comment && p.data === 'x-for' && !anchors.has(p) }
|
|
7
12
|
|
|
8
13
|
BINDERS.push({
|
|
9
14
|
match: (a) => a === 'x-for',
|
|
@@ -17,13 +22,14 @@ BINDERS.push({
|
|
|
17
22
|
// All server-rendered siblings carrying the same x-for are rows of this list; the first is the template.
|
|
18
23
|
const parent = el.parentElement!
|
|
19
24
|
const siblings = Array.from(parent.children).filter((c) => c.getAttribute('x-for') === raw)
|
|
25
|
+
const templates = new Set(siblings.filter(isTemplate))
|
|
20
26
|
siblings.forEach((s) => done.add(s))
|
|
21
27
|
const template = el.cloneNode(true) as Element
|
|
22
28
|
template.removeAttribute('x-for'); template.removeAttribute('hidden')
|
|
23
|
-
const anchor = document.createComment('x-for')
|
|
29
|
+
const anchor = document.createComment('x-for'); anchors.add(anchor)
|
|
24
30
|
parent.insertBefore(anchor, el)
|
|
25
|
-
// Server-rendered rows (not the
|
|
26
|
-
const adopt = siblings.filter((s) => !
|
|
31
|
+
// Server-rendered rows (not the zero-row template) are adopted in order on the first run.
|
|
32
|
+
const adopt = siblings.filter((s) => !templates.has(s))
|
|
27
33
|
if (!adopt.includes(el)) el.remove()
|
|
28
34
|
let firstRun = true
|
|
29
35
|
|
package/src/directives/index.ts
CHANGED
|
@@ -16,11 +16,21 @@ export function walkChildren(inst: Instance, el: Element, scope: Scope): void {
|
|
|
16
16
|
for (const child of Array.from(el.children)) {
|
|
17
17
|
if ('__scope' in child) continue // x-for row: already bound with its own scope
|
|
18
18
|
if (child.hasAttribute('x-for')) { bindElement(inst, child, scope); continue } // x-for owns the element (even when it is also a host)
|
|
19
|
-
if (child.hasAttribute(HOST_ATTR))
|
|
19
|
+
if (child.hasAttribute(HOST_ATTR)) { // nested host: its own instance binds it,
|
|
20
|
+
const pref = child.getAttribute('x-pref') // but the ref its caller passed is the caller's (F267/F288)
|
|
21
|
+
if (pref) bindRef(inst, child, pref)
|
|
22
|
+
continue
|
|
23
|
+
}
|
|
20
24
|
bindElement(inst, child, scope)
|
|
21
25
|
if (!child.hasAttribute('x-if')) walkChildren(inst, child, scope) // x-if owns its subtree
|
|
22
26
|
}
|
|
23
27
|
}
|
|
28
|
+
/** `x-ref` / `x-pref`: the instance's ref cell `name` holds `el` while it is bound. */
|
|
29
|
+
export function bindRef(inst: Instance, el: Element, name: string): void {
|
|
30
|
+
const r = inst.ref(name.trim())
|
|
31
|
+
r.current = el as HTMLElement
|
|
32
|
+
inst.onCleanup(() => { if (r.current === el) r.current = null })
|
|
33
|
+
}
|
|
24
34
|
export function bindElement(inst: Instance, el: Element, scope: Scope): void {
|
|
25
35
|
// Structural directives own the element: x-for/x-if re-create it from a template, so the
|
|
26
36
|
// remaining attributes are bound on the clones (with the right scope), never on this original.
|
package/src/directives/ref.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import { BINDERS } from '../core-api'
|
|
1
|
+
import { BINDERS, bindRef } from '../core-api'
|
|
2
2
|
BINDERS.push({
|
|
3
3
|
match: (a) => a === 'x-ref',
|
|
4
|
-
bind: ({ inst, el, raw }) =>
|
|
5
|
-
const r = inst.ref(raw.trim())
|
|
6
|
-
r.current = el as HTMLElement
|
|
7
|
-
inst.onCleanup(() => { if (r.current === el) r.current = null })
|
|
8
|
-
},
|
|
4
|
+
bind: ({ inst, el, raw }) => bindRef(inst, el, raw),
|
|
9
5
|
})
|
package/src/instance.ts
CHANGED
|
@@ -27,7 +27,7 @@ export class Instance {
|
|
|
27
27
|
* first paint, linked to its parent's current value): its first bind is not a first paint, so no tripwire. */
|
|
28
28
|
reseeded = false
|
|
29
29
|
/** Effects the component declared while its factory ran: they start once the instance is bound (x-ref, x-on), like React's commit (F97). */
|
|
30
|
-
private pending: Array<[() => void | (() => void), Dispose?]> | null = []
|
|
30
|
+
private pending: Array<[() => void | (() => void), Dispose?, number?]> | null = []
|
|
31
31
|
|
|
32
32
|
constructor(public host: HTMLElement, public name: string, public parent: Instance | null, initialProps: Record<string, unknown>) {
|
|
33
33
|
this.props = signal(initialProps)
|
|
@@ -35,13 +35,15 @@ export class Instance {
|
|
|
35
35
|
if (typeof __BRUST_DEV__ !== 'undefined' && __BRUST_DEV__) devTrack(this, true)
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
effect(fn: () => void | (() => void)): Dispose {
|
|
39
|
-
const d = coreEffect(fn); this.disposers.push(d); return d
|
|
38
|
+
effect(fn: () => void | (() => void), tier = 0): Dispose {
|
|
39
|
+
const d = coreEffect(fn, tier); this.disposers.push(d); return d
|
|
40
40
|
}
|
|
41
|
-
/** `ctx.effect`: deferred until `start()` while the factory runs, immediate afterwards.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
41
|
+
/** `ctx.effect`: deferred until `start()` while the factory runs, immediate afterwards. A compiled effect re-runs after
|
|
42
|
+
* the bindings: layout (`layout` true) before passive (F288, ruling 94404307 amended). */
|
|
43
|
+
private componentEffect(fn: () => void | (() => void), layout = false): Dispose {
|
|
44
|
+
const tier = layout ? 1 : 2
|
|
45
|
+
if (!this.pending) return this.effect(fn, tier)
|
|
46
|
+
const q: [() => void | (() => void), Dispose?, number?] = [fn, undefined, tier]
|
|
45
47
|
this.pending.push(q)
|
|
46
48
|
return () => { q[0] = () => {}; q[1]?.() }
|
|
47
49
|
}
|
|
@@ -57,7 +59,7 @@ export class Instance {
|
|
|
57
59
|
this.pending = null
|
|
58
60
|
for (const q of queue ?? []) {
|
|
59
61
|
if (this.disposed) break
|
|
60
|
-
try { q[1] = this.effect(q[0]) }
|
|
62
|
+
try { q[1] = this.effect(q[0], q[2]) }
|
|
61
63
|
catch (e) { console.error(`[brust] effect threw: ${this.name}`, e); this.dispose(); instances.delete(this.host); return false }
|
|
62
64
|
}
|
|
63
65
|
return true
|
|
@@ -70,7 +72,7 @@ export class Instance {
|
|
|
70
72
|
return r as { current: E | null }
|
|
71
73
|
}
|
|
72
74
|
ctx(): BehaviorCtx {
|
|
73
|
-
return { el: this.host, props: this.props, effect: (fn) => this.componentEffect(fn), onCleanup: (fn) => this.onCleanup(fn), ref: (n, init) => this.ref(n, init), late: this.late, items: () => collectionHooks.items(this.host) }
|
|
75
|
+
return { el: this.host, props: this.props, effect: (fn, layout) => this.componentEffect(fn, layout), onCleanup: (fn) => this.onCleanup(fn), ref: (n, init) => this.ref(n, init), late: this.late, items: () => collectionHooks.items(this.host) }
|
|
74
76
|
}
|
|
75
77
|
init(factory: BehaviorFactory): void {
|
|
76
78
|
const m = factory(this.ctx())
|
package/src/registry.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Dispose, Signal } from './signal'
|
|
|
3
3
|
export interface BehaviorCtx {
|
|
4
4
|
el: HTMLElement
|
|
5
5
|
props: Signal<Record<string, unknown>>
|
|
6
|
-
effect: (fn: () => void | (() => void)) => Dispose
|
|
6
|
+
effect: (fn: () => void | (() => void), layout?: boolean) => Dispose
|
|
7
7
|
onCleanup: (fn: () => void) => void
|
|
8
8
|
ref: <E extends Element = HTMLElement>(name: string, init?: unknown) => { current: E | null }
|
|
9
9
|
/** True when this instance mounted after the page booted (an x-if clone, an x-for row, a navigation): nothing of it was server-rendered. */
|
package/src/signal.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// A small push-pull reactive core. Signals push "dirty" to subscribers; computeds pull lazily.
|
|
2
2
|
type Subscriber = { mark(): void; deps: Set<Node> }
|
|
3
3
|
type Node = { subs: Set<Subscriber> }
|
|
4
|
-
type Runnable = { disposed: boolean; run(): void }
|
|
4
|
+
type Runnable = { disposed: boolean; run(): void; tier?: number; id?: number }
|
|
5
5
|
interface ReactiveCtx { currentSub: Subscriber | null; batchDepth: number; pending: Set<Runnable> }
|
|
6
6
|
|
|
7
7
|
const CTX = Symbol.for('brust.reactive.ctx') // M3-F C7
|
|
@@ -23,8 +23,13 @@ function flush() {
|
|
|
23
23
|
if (ctx.batchDepth > 0) return
|
|
24
24
|
const pending = ctx.pending
|
|
25
25
|
while (pending.size) {
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
// F288 (ruling 94404307 amended): a re-run follows React's commit order: every binding of the change (tier 0, in
|
|
27
|
+
// notify order), then compiled layout effects (1), then compiled passive effects (2), each in declaration order.
|
|
28
|
+
const all = Array.from(pending)
|
|
29
|
+
const t = Math.min(...all.map((e) => e.tier || 0))
|
|
30
|
+
// a stable sort: bindings (t = 0) keep their notify order
|
|
31
|
+
const run = all.filter((e) => (e.tier || 0) === t).sort((a, b) => t && a.id! - b.id!)
|
|
32
|
+
for (const e of run) pending.delete(e), e.disposed || e.run()
|
|
28
33
|
}
|
|
29
34
|
}
|
|
30
35
|
|
|
@@ -67,9 +72,11 @@ export function computed<T>(fn: () => T): Computed<T> {
|
|
|
67
72
|
// Cleanups run untracked: a read inside one must not subscribe whichever effect is running around it.
|
|
68
73
|
function runCleanup(c: () => void) { const prev = ctx.currentSub; ctx.currentSub = null; try { c() } finally { ctx.currentSub = prev } }
|
|
69
74
|
|
|
75
|
+
let ids = 0
|
|
70
76
|
class EffectImpl implements Subscriber {
|
|
71
77
|
deps = new Set<Node>(); disposed = false; cleanup: void | (() => void) = undefined
|
|
72
|
-
|
|
78
|
+
readonly id = ++ids
|
|
79
|
+
constructor(private fn: () => void | (() => void), readonly tier = 0) {}
|
|
73
80
|
mark() { ctx.pending.add(this) }
|
|
74
81
|
run() {
|
|
75
82
|
if (this.disposed) return
|
|
@@ -86,8 +93,9 @@ class EffectImpl implements Subscriber {
|
|
|
86
93
|
}
|
|
87
94
|
}
|
|
88
95
|
|
|
89
|
-
|
|
90
|
-
|
|
96
|
+
/** `tier`: 0 a binding, 1 a compiled layout effect, 2 a compiled passive effect (the order of re-runs). */
|
|
97
|
+
export function effect(fn: () => void | (() => void), tier = 0): Dispose {
|
|
98
|
+
const e = new EffectImpl(fn, tier)
|
|
91
99
|
try { e.run() } catch (x) { e.dispose(); throw x } // F98: a first run that throws must not stay subscribed
|
|
92
100
|
return () => e.dispose()
|
|
93
101
|
}
|