@abide/abide 0.37.0 → 0.38.1
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/AGENTS.md +1 -1
- package/CHANGELOG.md +38 -0
- package/package.json +2 -1
- package/src/abideResolverPlugin.ts +3 -17
- package/src/appEntry.ts +18 -8
- package/src/controlServerWorker.ts +8 -1
- package/src/lib/cli/parseArgvForRpc.ts +8 -3
- package/src/lib/mcp/createMcpServer.ts +8 -0
- package/src/lib/mcp/dispatchMcpRequest.ts +20 -2
- package/src/lib/mcp/toolResultFromResponse.ts +5 -0
- package/src/lib/server/rpc/parseArgs.ts +12 -1
- package/src/lib/server/runtime/acceptsGzip.ts +10 -1
- package/src/lib/server/runtime/gzipResponse.ts +2 -1
- package/src/lib/server/runtime/snapshotEntryFromCache.ts +2 -1
- package/src/lib/server/runtime/streamFromIterator.ts +8 -0
- package/src/lib/server/sockets/createSocketDispatcher.ts +7 -1
- package/src/lib/server/sockets/defineSocket.ts +6 -1
- package/src/lib/shared/cache.ts +6 -0
- package/src/lib/shared/createPushIterator.ts +7 -2
- package/src/lib/ui/COMPONENT_WRAPPER_PREFIX.ts +5 -0
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +1 -0
- package/src/lib/ui/compile/abideUiPlugin.ts +2 -1
- package/src/lib/ui/compile/asOutlet.ts +30 -0
- package/src/lib/ui/compile/compileModule.ts +13 -9
- package/src/lib/ui/compile/generateBuild.ts +83 -72
- package/src/lib/ui/compile/generateSSR.ts +56 -37
- package/src/lib/ui/compile/isAnchorPositioned.ts +19 -0
- package/src/lib/ui/compile/isControlFlow.ts +3 -1
- package/src/lib/ui/compile/isTextLeaf.ts +1 -1
- package/src/lib/ui/compile/parseTemplate.ts +10 -1
- package/src/lib/ui/compile/renameSignalRefs.ts +25 -2
- package/src/lib/ui/compile/skeletonContext.ts +97 -35
- package/src/lib/ui/compile/skeletonable.ts +3 -2
- package/src/lib/ui/compile/types/SkeletonContext.ts +6 -0
- package/src/lib/ui/dom/appendSnippet.ts +60 -20
- package/src/lib/ui/dom/commentData.ts +14 -0
- package/src/lib/ui/dom/disposeRange.ts +21 -0
- package/src/lib/ui/dom/fillBefore.ts +10 -0
- package/src/lib/ui/dom/fillBoundary.ts +38 -0
- package/src/lib/ui/dom/fillRange.ts +30 -0
- package/src/lib/ui/dom/hydrate.ts +11 -21
- package/src/lib/ui/dom/mount.ts +16 -25
- package/src/lib/ui/dom/mountChild.ts +27 -14
- package/src/lib/ui/dom/mountRange.ts +45 -0
- package/src/lib/ui/dom/mountSlot.ts +7 -2
- package/src/lib/ui/dom/outlet.ts +62 -0
- package/src/lib/ui/dom/scopeLabel.ts +13 -7
- package/src/lib/ui/dom/skeleton.ts +11 -27
- package/src/lib/ui/dom/withScope.ts +33 -0
- package/src/lib/ui/installHotBridge.ts +2 -0
- package/src/lib/ui/persist.ts +4 -1
- package/src/lib/ui/renderChain.ts +23 -15
- package/src/lib/ui/router.ts +78 -38
- package/src/lib/ui/runtime/OUTLET_MARKER.ts +10 -0
- package/src/lib/ui/runtime/OUTLET_TAG.ts +5 -6
- package/src/lib/ui/runtime/PENDING_OUTLET.ts +8 -0
- package/src/lib/ui/runtime/captureModelDoc.ts +12 -13
- package/src/lib/ui/runtime/createDoc.ts +20 -7
- package/src/lib/ui/runtime/hotReplace.ts +14 -10
- package/src/lib/ui/runtime/types/HotInstance.ts +11 -7
- package/src/lib/ui/runtime/types/Route.ts +6 -5
- package/src/lib/ui/runtime/types/UiComponent.ts +5 -0
- package/src/lib/ui/seedStreamedResolution.ts +10 -1
- package/src/lib/ui/compile/componentWrapperTag.ts +0 -13
- package/src/lib/ui/runtime/firstOutlet.ts +0 -22
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { OUTLET_TAG } from '../runtime/OUTLET_TAG.ts'
|
|
2
|
+
import { isAnchorPositioned } from './isAnchorPositioned.ts'
|
|
1
3
|
import { isControlFlow } from './isControlFlow.ts'
|
|
2
4
|
import { isTextLeaf } from './isTextLeaf.ts'
|
|
3
5
|
import { skeletonable } from './skeletonable.ts'
|
|
@@ -5,74 +7,134 @@ import type { SkeletonContext } from './types/SkeletonContext.ts'
|
|
|
5
7
|
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
6
8
|
|
|
7
9
|
/*
|
|
8
|
-
The single source of truth for where skeleton markers go
|
|
9
|
-
per node, whether it sits inside a parser-backed skeleton clone
|
|
10
|
-
whether its reactive text is interleaved (`markText`) — the two facts
|
|
11
|
-
`<!--a-->` anchor placement
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
anchor the client never emitted, desyncing hydration.
|
|
10
|
+
The single source of truth for where skeleton markers go AND what their hole indices are.
|
|
11
|
+
One top-down walk records, per node, whether it sits inside a parser-backed skeleton clone
|
|
12
|
+
(`inSkeleton`) and whether its reactive text is interleaved (`markText`) — the two facts
|
|
13
|
+
that decide `<!--a-->` anchor placement — and assigns each hole its `el`/`an` index.
|
|
14
|
+
`generateBuild` reads these indices instead of threading its own counter through a second
|
|
15
|
+
document-order walk, so the numbering cannot drift from the decisions: one walk owns both.
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
The index assignment is scoped per skeleton root (a `skeletonable` element not already in a
|
|
18
|
+
skeleton — the unit `generateSkeleton` instantiates with `{ el: 0, an: 0 }`). `el` numbers
|
|
19
|
+
element holes in pre-order; `an` numbers anchor holes (interleaved reactive text PARTS,
|
|
20
|
+
control-flow blocks, child components, `<slot>` outlets) in
|
|
21
|
+
document order — the orders the runtime's `indexElementHoles`/`scanAnchors` re-derive from
|
|
22
|
+
the realized DOM, so the compile-time numbers and the runtime positions line up.
|
|
23
|
+
|
|
24
|
+
A fresh-context boundary resets to NOT-in-skeleton (and to no active counter), because the
|
|
25
|
+
content there is built by its own runtime (a control-flow block's branch, a component's slot
|
|
26
|
+
content, a `<slot>`'s fallback, a snippet's body) — never cloned by the enclosing skeleton.
|
|
21
27
|
*/
|
|
22
28
|
export function skeletonContext(nodes: TemplateNode[]): SkeletonContext {
|
|
23
29
|
const inSkeleton = new WeakMap<TemplateNode, boolean>()
|
|
24
30
|
const markText = new WeakMap<TemplateNode, boolean>()
|
|
31
|
+
/* Element holes keyed by node; anchor holes keyed by node (control-flow/component/slot)
|
|
32
|
+
OR by the reactive text PART object (a text node carries one anchor per reactive part). */
|
|
33
|
+
const elIndex = new WeakMap<TemplateNode, number>()
|
|
34
|
+
const anIndex = new WeakMap<object, number>()
|
|
35
|
+
|
|
36
|
+
type Counter = { el: number; an: number }
|
|
25
37
|
|
|
26
|
-
/* Walk `node` carrying the context
|
|
27
|
-
|
|
28
|
-
function visit(
|
|
38
|
+
/* Walk `node` carrying the context AND the active skeleton counter that apply AT it
|
|
39
|
+
(`counter === undefined` outside any skeleton — no holes are numbered there). */
|
|
40
|
+
function visit(
|
|
41
|
+
node: TemplateNode,
|
|
42
|
+
nodeInSkeleton: boolean,
|
|
43
|
+
nodeMarkText: boolean,
|
|
44
|
+
counter: Counter | undefined,
|
|
45
|
+
): void {
|
|
29
46
|
inSkeleton.set(node, nodeInSkeleton)
|
|
30
47
|
markText.set(node, nodeMarkText)
|
|
31
48
|
|
|
32
|
-
/* Control-flow
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
49
|
+
/* Control-flow blocks, components, and snippets are fresh build contexts. The node
|
|
50
|
+
ITSELF is a hole in the enclosing skeleton (an `<!--a-->` anchor for a block OR a
|
|
51
|
+
component — both mount as a marker-bounded range at that anchor); its children
|
|
52
|
+
re-enter the skeleton only via their own roots. */
|
|
36
53
|
if (isControlFlow(node) || node.kind === 'component' || node.kind === 'snippet') {
|
|
54
|
+
/* A block or a component takes an anchor only inside an enclosing skeleton (a
|
|
55
|
+
standalone one routes through `generateIf`/`mountChild`, not the skeleton path);
|
|
56
|
+
a snippet declares a builder and is never anchor-positioned (`isAnchorPositioned`). */
|
|
57
|
+
if (counter !== undefined && isAnchorPositioned(node)) {
|
|
58
|
+
anIndex.set(node, counter.an++)
|
|
59
|
+
}
|
|
37
60
|
for (const child of childrenOf(node)) {
|
|
38
|
-
visit(child, false, false)
|
|
61
|
+
visit(child, false, false, undefined)
|
|
39
62
|
}
|
|
40
63
|
return
|
|
41
64
|
}
|
|
42
|
-
/* A `branch`/`case` is a transparent grouping inside its control-flow block
|
|
43
|
-
|
|
44
|
-
|
|
65
|
+
/* A `branch`/`case` is a transparent grouping inside its control-flow block — pass the
|
|
66
|
+
already-reset context (and absent counter) through so a skeletonable element inside
|
|
67
|
+
it opens its own skeleton. */
|
|
45
68
|
if (node.kind === 'branch' || node.kind === 'case') {
|
|
46
69
|
for (const child of node.children) {
|
|
47
|
-
visit(child, nodeInSkeleton, nodeMarkText)
|
|
70
|
+
visit(child, nodeInSkeleton, nodeMarkText, counter)
|
|
71
|
+
}
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
if (node.kind === 'text') {
|
|
75
|
+
/* Interleaved reactive text (markText true): each reactive part takes an `<!--a-->`
|
|
76
|
+
anchor, numbered in document order. A text-leaf's text (markText false) binds
|
|
77
|
+
marker-free via the element, so its parts take no anchor. */
|
|
78
|
+
if (counter !== undefined && nodeMarkText) {
|
|
79
|
+
for (const part of node.parts) {
|
|
80
|
+
if (part.kind !== 'static') {
|
|
81
|
+
anIndex.set(part, counter.an++)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
48
84
|
}
|
|
49
85
|
return
|
|
50
86
|
}
|
|
51
87
|
if (node.kind !== 'element') {
|
|
52
|
-
return //
|
|
88
|
+
return // script / style carry no skeleton children and no hole
|
|
53
89
|
}
|
|
54
|
-
if (node.tag === 'slot') {
|
|
55
|
-
/*
|
|
56
|
-
|
|
90
|
+
if (node.tag === 'slot' || node.tag === OUTLET_TAG) {
|
|
91
|
+
/* A component `<slot>` content fill OR a layout's `OUTLET_TAG` router fill point
|
|
92
|
+
(`asOutlet`) is an anchor hole in the enclosing skeleton — both mount a marker
|
|
93
|
+
range at the anchor (`mountSlot` / `outlet`). A `<slot>`'s children are its
|
|
94
|
+
fallback (a fresh context); an outlet has none. */
|
|
95
|
+
if (counter !== undefined) {
|
|
96
|
+
anIndex.set(node, counter.an++)
|
|
97
|
+
}
|
|
57
98
|
for (const child of node.children) {
|
|
58
|
-
visit(child, false, false)
|
|
99
|
+
visit(child, false, false, undefined)
|
|
59
100
|
}
|
|
60
101
|
return
|
|
61
102
|
}
|
|
62
|
-
/* A skeletonable element not already in a skeleton
|
|
63
|
-
|
|
64
|
-
|
|
103
|
+
/* A skeletonable element not already in a skeleton OPENS one (a fresh counter — the
|
|
104
|
+
`generateSkeleton` unit). An element already in a skeleton uses the enclosing
|
|
105
|
+
counter. A static element outside any skeleton numbers nothing. */
|
|
106
|
+
const opensSkeleton = !nodeInSkeleton && skeletonable(node)
|
|
65
107
|
const childInSkeleton = nodeInSkeleton || skeletonable(node)
|
|
108
|
+
const effectiveCounter = nodeInSkeleton
|
|
109
|
+
? counter
|
|
110
|
+
: opensSkeleton
|
|
111
|
+
? { el: 0, an: 0 }
|
|
112
|
+
: undefined
|
|
66
113
|
const childMarkText = childInSkeleton && !isTextLeaf(node)
|
|
114
|
+
|
|
115
|
+
if (effectiveCounter !== undefined) {
|
|
116
|
+
/* The element is a located hole when it carries a reactive attr/listener/bind, or
|
|
117
|
+
binds text-leaf reactive text on itself. Take its `el` index BEFORE recursing,
|
|
118
|
+
so holes number in pre-order — the order the runtime's path walk produces them. */
|
|
119
|
+
const hasReactiveAttr = node.attrs.some((attr) => attr.kind !== 'static')
|
|
120
|
+
const reactiveTextChild = node.children.find(
|
|
121
|
+
(child) =>
|
|
122
|
+
child.kind === 'text' && child.parts.some((part) => part.kind !== 'static'),
|
|
123
|
+
)
|
|
124
|
+
const textLeafBind = reactiveTextChild !== undefined && isTextLeaf(node)
|
|
125
|
+
if (hasReactiveAttr || textLeafBind) {
|
|
126
|
+
elIndex.set(node, effectiveCounter.el++)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
67
129
|
for (const child of node.children) {
|
|
68
|
-
visit(child, childInSkeleton, childMarkText)
|
|
130
|
+
visit(child, childInSkeleton, childMarkText, effectiveCounter)
|
|
69
131
|
}
|
|
70
132
|
}
|
|
71
133
|
|
|
72
134
|
for (const node of nodes) {
|
|
73
|
-
visit(node, false, false)
|
|
135
|
+
visit(node, false, false, undefined)
|
|
74
136
|
}
|
|
75
|
-
return { inSkeleton, markText }
|
|
137
|
+
return { inSkeleton, markText, elIndex, anIndex }
|
|
76
138
|
}
|
|
77
139
|
|
|
78
140
|
/* The child list of any node that has one (control-flow, component, snippet, element);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { OUTLET_TAG } from '../runtime/OUTLET_TAG.ts'
|
|
1
2
|
import { isControlFlow } from './isControlFlow.ts'
|
|
2
3
|
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
3
4
|
|
|
@@ -22,7 +23,7 @@ function skeletonStructure(node: TemplateNode): boolean {
|
|
|
22
23
|
if (node.kind !== 'element') {
|
|
23
24
|
return false // standalone branch|case
|
|
24
25
|
}
|
|
25
|
-
if (node.tag === 'slot') {
|
|
26
|
+
if (node.tag === 'slot' || node.tag === OUTLET_TAG) {
|
|
26
27
|
return true
|
|
27
28
|
}
|
|
28
29
|
return node.children.every(skeletonStructure)
|
|
@@ -43,7 +44,7 @@ function hasHole(node: TemplateNode): boolean {
|
|
|
43
44
|
return node.parts.some((part) => part.kind !== 'static')
|
|
44
45
|
}
|
|
45
46
|
if (node.kind === 'element') {
|
|
46
|
-
if (node.tag === 'slot') {
|
|
47
|
+
if (node.tag === 'slot' || node.tag === OUTLET_TAG) {
|
|
47
48
|
return true
|
|
48
49
|
}
|
|
49
50
|
return node.attrs.some((attr) => attr.kind !== 'static') || node.children.some(hasHole)
|
|
@@ -12,4 +12,10 @@ filled by one shared `skeletonContext` pass over the parsed tree.
|
|
|
12
12
|
export type SkeletonContext = {
|
|
13
13
|
inSkeleton: WeakMap<TemplateNode, boolean>
|
|
14
14
|
markText: WeakMap<TemplateNode, boolean>
|
|
15
|
+
/* Per-hole indices assigned in the same walk, so `generateBuild` reads its `sk.el`/`sk.an`
|
|
16
|
+
numbering rather than re-deriving it. `elIndex` keyed by element/component node;
|
|
17
|
+
`anIndex` keyed by control-flow/slot node OR by a reactive text PART object (a text node
|
|
18
|
+
carries one anchor per reactive part). */
|
|
19
|
+
elIndex: WeakMap<TemplateNode, number>
|
|
20
|
+
anIndex: WeakMap<object, number>
|
|
15
21
|
}
|
|
@@ -1,34 +1,74 @@
|
|
|
1
1
|
import { snippetPayload } from '../../shared/snippet.ts'
|
|
2
|
-
import {
|
|
2
|
+
import { effect } from '../effect.ts'
|
|
3
3
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
4
|
+
import { scope } from '../runtime/scope.ts'
|
|
5
|
+
import { scopeGroup } from '../runtime/scopeGroup.ts'
|
|
6
|
+
import { clearBetween } from './clearBetween.ts'
|
|
7
|
+
import { fillBefore } from './fillBefore.ts'
|
|
8
|
+
import { openMarker } from './openMarker.ts'
|
|
4
9
|
|
|
5
10
|
/*
|
|
6
|
-
A `{snippet(args)}` interpolation: mount the branded builder's nodes
|
|
7
|
-
|
|
8
|
-
places it correctly among siblings
|
|
9
|
-
component scope
|
|
10
|
-
|
|
11
|
+
A `{snippet(args)}` interpolation: mount the branded builder's nodes in a range
|
|
12
|
+
bounded by two comment markers. The content builds straight into `parent` —
|
|
13
|
+
sequential build order places it correctly among siblings — and its effects join
|
|
14
|
+
the surrounding component scope, so the body's reactive reads update fine-grained.
|
|
15
|
+
|
|
16
|
+
The CALL is reactive in its arguments: an effect re-reads `read()` so a change in
|
|
17
|
+
the argument expression (e.g. `{row(items())}`) tears the range down and rebuilds
|
|
18
|
+
with fresh args. `read()` returns a fresh builder closing over freshly-evaluated
|
|
19
|
+
args each call, so there is nothing to diff — the snippet re-mounts as a unit, the
|
|
20
|
+
same coarse model as `when`/`each` (args behave like props). The body's own reads
|
|
21
|
+
stay fine-grained within a single mount; the outer effect tracks only the args,
|
|
22
|
+
since `fillBefore` wraps the body in its own scope.
|
|
11
23
|
|
|
12
24
|
On hydrate the builder runs against the server-rendered nodes between the
|
|
13
25
|
`<!--abide:snippet-->`/`<!--/abide:snippet-->` markers — its `skeleton`/`appendText`
|
|
14
|
-
claim them in place
|
|
15
|
-
the
|
|
26
|
+
claim them in place — and the effect's first run is a no-op that only subscribes to
|
|
27
|
+
the args; a later argument change rebuilds fresh (the SSR markers stay as the range).
|
|
16
28
|
*/
|
|
17
29
|
// @documentation plumbing
|
|
18
30
|
export function appendSnippet(parent: Node, read: () => unknown): void {
|
|
19
|
-
const builder = snippetPayload(read())
|
|
20
|
-
if (typeof builder !== 'function') {
|
|
21
|
-
return
|
|
22
|
-
}
|
|
23
|
-
const mount = builder as (host: Node) => void
|
|
24
31
|
const hydration = RENDER.hydration
|
|
32
|
+
/* Mount scopes register with the owner so they dispose on owner teardown, not
|
|
33
|
+
only on an argument-driven rebuild via clearBetween. */
|
|
34
|
+
const group = scopeGroup()
|
|
35
|
+
let dispose: (() => void) | undefined
|
|
36
|
+
|
|
37
|
+
/* The branded builder, or undefined for anything else (a non-snippet value
|
|
38
|
+
mounts nothing — the range stays empty until the args yield a builder). */
|
|
39
|
+
const builderOf = (): ((host: Node) => void) | undefined => {
|
|
40
|
+
const payload = snippetPayload(read())
|
|
41
|
+
return typeof payload === 'function' ? (payload as (host: Node) => void) : undefined
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let open: Comment
|
|
45
|
+
let close: Comment
|
|
25
46
|
if (hydration !== undefined) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
47
|
+
open = openMarker(parent, 'abide:snippet')
|
|
48
|
+
const builder = builderOf()
|
|
49
|
+
if (builder !== undefined) {
|
|
50
|
+
dispose = group.track(scope(() => builder(parent))) // content claims the SSR nodes in place
|
|
51
|
+
}
|
|
52
|
+
close = openMarker(parent, '/abide:snippet')
|
|
53
|
+
} else {
|
|
54
|
+
open = openMarker(parent, 'abide:snippet')
|
|
55
|
+
close = openMarker(parent, '/abide:snippet')
|
|
56
|
+
const builder = builderOf()
|
|
57
|
+
if (builder !== undefined) {
|
|
58
|
+
dispose = group.track(fillBefore(close, builder))
|
|
59
|
+
}
|
|
32
60
|
}
|
|
33
|
-
|
|
61
|
+
|
|
62
|
+
/* The initial mount is built above (create or hydrate); the first effect run only
|
|
63
|
+
subscribes to the args via `builderOf`, then later argument changes rebuild. */
|
|
64
|
+
let first = true
|
|
65
|
+
effect(() => {
|
|
66
|
+
const builder = builderOf()
|
|
67
|
+
if (first) {
|
|
68
|
+
first = false
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
clearBetween(open, close, dispose)
|
|
72
|
+
dispose = builder !== undefined ? group.track(fillBefore(close, builder)) : undefined
|
|
73
|
+
})
|
|
34
74
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* A comment node's data, or undefined for elements/text. A comment is a node that is
|
|
2
|
+
neither an element (`hasAttribute`) nor a text node (`splitText`); the mini-dom
|
|
3
|
+
exposes no `nodeType`, so detect by method. Shared by every marker-range scan
|
|
4
|
+
(`skeleton`'s anchor walk, `outlet`'s close-marker skip) so the convention is probed
|
|
5
|
+
one way everywhere. */
|
|
6
|
+
export function commentData(node: Node): string | undefined {
|
|
7
|
+
if (
|
|
8
|
+
typeof (node as Element).hasAttribute === 'function' ||
|
|
9
|
+
typeof (node as Text).splitText === 'function'
|
|
10
|
+
) {
|
|
11
|
+
return undefined
|
|
12
|
+
}
|
|
13
|
+
return (node as Comment).data
|
|
14
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Scope } from '../types/Scope.ts'
|
|
2
|
+
import { clearBetween } from './clearBetween.ts'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
The standard teardown for a marker-bounded range (component, layout/page boundary,
|
|
6
|
+
slot): stop the content's reactivity, dispose its lexical scope, and clear the nodes
|
|
7
|
+
between the markers — leaving the markers in place so a hot swap rebuilds the range.
|
|
8
|
+
Shared by every range mount (`fillRange`, `mountRange`, `fillBoundary`) so the one
|
|
9
|
+
disposer contract lives in a single place.
|
|
10
|
+
*/
|
|
11
|
+
export function disposeRange(
|
|
12
|
+
scoped: { stop: () => void; lexical: Scope },
|
|
13
|
+
start: Comment,
|
|
14
|
+
end: Comment,
|
|
15
|
+
): () => void {
|
|
16
|
+
return () => {
|
|
17
|
+
scoped.stop()
|
|
18
|
+
scoped.lexical.dispose()
|
|
19
|
+
clearBetween(start, end)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -18,6 +18,16 @@ would otherwise make the build helpers claim SSR nodes that don't exist for fres
|
|
|
18
18
|
content. The same cursor is restored after (mirrors awaitBlock/tryBlock/each).
|
|
19
19
|
*/
|
|
20
20
|
export function fillBefore(end: Node, content: (into: Node) => void): () => void {
|
|
21
|
+
/* A control-flow effect can fire one final time after its block was already
|
|
22
|
+
detached — e.g. an enclosing await/each block tears the branch down in the same
|
|
23
|
+
microtask flush that re-ran this effect, before the owner scope disposes it. The
|
|
24
|
+
end marker then has no live parent, so there is nowhere to build into; inserting
|
|
25
|
+
a fragment before a parentless comment throws HierarchyRequestError ("would yield
|
|
26
|
+
an incorrect node tree"). Skip the rebuild — owner teardown disposes this dead
|
|
27
|
+
block anyway. */
|
|
28
|
+
if (!end.parentNode) {
|
|
29
|
+
return () => {}
|
|
30
|
+
}
|
|
21
31
|
const fragment = document.createDocumentFragment()
|
|
22
32
|
const previousHydration = RENDER.hydration
|
|
23
33
|
RENDER.hydration = undefined
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { RENDER } from '../runtime/RENDER.ts'
|
|
2
|
+
import { scope } from '../runtime/scope.ts'
|
|
3
|
+
import type { UiProps } from '../runtime/types/UiProps.ts'
|
|
4
|
+
import { disposeRange } from './disposeRange.ts'
|
|
5
|
+
import { fillRange } from './fillRange.ts'
|
|
6
|
+
import { withScope } from './withScope.ts'
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
Mounts a chain layer (layout or page) into an EXISTING outlet boundary — the markers
|
|
10
|
+
a parent layout's `<slot/>` left (`outlet`), or the router's root boundary in `#app`.
|
|
11
|
+
The router fills these boundaries instead of mounting into a host element, so the whole
|
|
12
|
+
page/layout chain composes through one range model (no `<abide-outlet>` wrapper).
|
|
13
|
+
|
|
14
|
+
Create: build the layer's content into a fragment that lands just before `close`
|
|
15
|
+
(`fillRange`). Hydrate: claim the server content in place — park the parent cursor at
|
|
16
|
+
`open.nextSibling` and let the build adopt the existing nodes (the outlet markers
|
|
17
|
+
themselves are located via `PENDING_OUTLET`, not claimed through the cursor). The disposer
|
|
18
|
+
stops the layer's reactivity and clears the boundary — the router calls it to tear a
|
|
19
|
+
divergent layer down before rebuilding the same boundary.
|
|
20
|
+
*/
|
|
21
|
+
// @documentation plumbing
|
|
22
|
+
export function fillBoundary(
|
|
23
|
+
open: Comment,
|
|
24
|
+
close: Comment,
|
|
25
|
+
build: (host: Node, props?: UiProps) => void,
|
|
26
|
+
props: UiProps | undefined,
|
|
27
|
+
label: string | undefined,
|
|
28
|
+
): { dispose: () => void } {
|
|
29
|
+
const hydration = RENDER.hydration
|
|
30
|
+
if (hydration === undefined) {
|
|
31
|
+
return fillRange(open, close, build, props, label)
|
|
32
|
+
}
|
|
33
|
+
/* Hydrate: adopt the server content between the markers in place. */
|
|
34
|
+
const parent = open.parentNode as Node
|
|
35
|
+
hydration.next.set(parent, open.nextSibling)
|
|
36
|
+
const scoped = withScope(label, () => scope(() => build(parent, props)))
|
|
37
|
+
return { dispose: disposeRange(scoped, open, close) }
|
|
38
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { UiProps } from '../runtime/types/UiProps.ts'
|
|
2
|
+
import { disposeRange } from './disposeRange.ts'
|
|
3
|
+
import { fillBefore } from './fillBefore.ts'
|
|
4
|
+
import { withScope } from './withScope.ts'
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
Builds a component's content fresh between two existing range markers (the create
|
|
8
|
+
path), under the component's own lexical scope and render pass — the range analog of
|
|
9
|
+
`mount` for a nested child. `build` appends into a fragment (via `fillBefore`) that
|
|
10
|
+
lands just before `end`, so the content sits in the `[ … ]` range the child mounts
|
|
11
|
+
into rather than at the parent's tail; that range is what makes a component
|
|
12
|
+
selector-transparent (a true direct child of its parent, no `<abide-name>` wrapper).
|
|
13
|
+
|
|
14
|
+
Brackets a render pass (a nested child continues the parent's block-id counter) and
|
|
15
|
+
establishes the child's lexical scope in `awaiting` mode so it adopts the model doc
|
|
16
|
+
its first `doc()` creates. The disposer stops the content's reactivity, disposes the
|
|
17
|
+
lexical scope, and clears the range — leaving the markers, so a hot swap rebuilds in
|
|
18
|
+
place. Shared by `mountRange` (create branch) and `hotReplace` (re-fill on edit).
|
|
19
|
+
*/
|
|
20
|
+
// @documentation plumbing
|
|
21
|
+
export function fillRange(
|
|
22
|
+
start: Comment,
|
|
23
|
+
end: Comment,
|
|
24
|
+
build: (host: Node, props?: UiProps) => void,
|
|
25
|
+
props: UiProps | undefined,
|
|
26
|
+
label: string | undefined,
|
|
27
|
+
): { start: Comment; end: Comment; dispose: () => void } {
|
|
28
|
+
const scoped = withScope(label, () => fillBefore(end, (fragment) => build(fragment, props)))
|
|
29
|
+
return { start, end, dispose: disposeRange(scoped, start, end) }
|
|
30
|
+
}
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import { createScope } from '../createScope.ts'
|
|
2
|
-
import { CURRENT_SCOPE } from '../runtime/CURRENT_SCOPE.ts'
|
|
3
|
-
import { enterRenderPass } from '../runtime/enterRenderPass.ts'
|
|
4
|
-
import { exitRenderPass } from '../runtime/exitRenderPass.ts'
|
|
5
1
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
6
2
|
import { scope } from '../runtime/scope.ts'
|
|
7
3
|
import { scopeLabel } from './scopeLabel.ts'
|
|
4
|
+
import { withScope } from './withScope.ts'
|
|
8
5
|
|
|
9
6
|
/*
|
|
10
7
|
Adopts existing server-rendered DOM instead of rebuilding it. Runs `build(host)`
|
|
@@ -15,31 +12,24 @@ focus/scroll). Returns a disposer.
|
|
|
15
12
|
|
|
16
13
|
Adopts the server DOM in place across the framework: static structure (elements
|
|
17
14
|
+ text + bindings), `if`/`else`, keyed `each`, `switch`, `try`, and child
|
|
18
|
-
components (with slots) — they hydrate automatically because
|
|
19
|
-
claimed while hydration is still active. `await` adopts too when it can resume
|
|
15
|
+
components (with slots) — they hydrate automatically because a child's marker range
|
|
16
|
+
is claimed while hydration is still active (see `mountRange`). `await` adopts too when it can resume
|
|
20
17
|
the value (a streamed `RESUME[id]` or a warm-sync/cache read); only a genuinely-
|
|
21
18
|
pending `await` — no resume, not cache-warm — discards its boundary and builds
|
|
22
19
|
the pending branch fresh (see `awaitBlock`).
|
|
23
20
|
*/
|
|
24
21
|
// @documentation plumbing
|
|
25
|
-
export function hydrate(
|
|
22
|
+
export function hydrate(
|
|
23
|
+
host: Element,
|
|
24
|
+
build: (host: Element, props: unknown) => void,
|
|
25
|
+
props?: unknown,
|
|
26
|
+
): () => void {
|
|
26
27
|
const previous = RENDER.hydration
|
|
27
28
|
RENDER.hydration = { next: new Map() }
|
|
28
|
-
enterRenderPass()
|
|
29
|
-
/* Same lexical scope establishment as `mount` — a hydrated component owns a scope
|
|
30
|
-
too, adopting the model its build adopts. */
|
|
31
|
-
const parentScope = CURRENT_SCOPE.current
|
|
32
|
-
const lexical = createScope({}, parentScope, true, scopeLabel(host))
|
|
33
|
-
CURRENT_SCOPE.current = lexical
|
|
34
29
|
try {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
} finally {
|
|
39
|
-
exitRenderPass()
|
|
40
|
-
CURRENT_SCOPE.current = parentScope
|
|
41
|
-
}
|
|
42
|
-
})
|
|
30
|
+
/* Same shared mount core as `mount` (see `withScope`) — a hydrated component owns a
|
|
31
|
+
scope too, adopting the model its build adopts — run with the claim cursor active. */
|
|
32
|
+
const { stop, lexical } = withScope(scopeLabel(host), () => scope(() => build(host, props)))
|
|
43
33
|
return () => {
|
|
44
34
|
stop()
|
|
45
35
|
lexical.dispose()
|
package/src/lib/ui/dom/mount.ts
CHANGED
|
@@ -1,38 +1,29 @@
|
|
|
1
|
-
import { createScope } from '../createScope.ts'
|
|
2
|
-
import { CURRENT_SCOPE } from '../runtime/CURRENT_SCOPE.ts'
|
|
3
|
-
import { enterRenderPass } from '../runtime/enterRenderPass.ts'
|
|
4
|
-
import { exitRenderPass } from '../runtime/exitRenderPass.ts'
|
|
5
1
|
import { scope } from '../runtime/scope.ts'
|
|
6
2
|
import { scopeLabel } from './scopeLabel.ts'
|
|
3
|
+
import { withScope } from './withScope.ts'
|
|
7
4
|
|
|
8
5
|
/*
|
|
9
|
-
Mounts a
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
bindings below). This is the runtime
|
|
6
|
+
Mounts a top-level page/layout into `host` (the router's outlet/root element): runs
|
|
7
|
+
`build(host, props)` under an ownership scope so every binding it creates is
|
|
8
|
+
collected, and returns a disposer that stops all reactivity and clears the host.
|
|
9
|
+
`build` appends its nodes to `host` (via the dom bindings below). This is the runtime
|
|
10
|
+
entry the router calls; a NESTED child instead mounts as a marker range (see
|
|
11
|
+
`mountRange`), so it leaves no wrapper element.
|
|
13
12
|
|
|
14
13
|
Brackets a render pass so the outermost mount resets the block-id counter and an
|
|
15
14
|
inlined child component's mount continues it — keeping await/try ids aligned with
|
|
16
15
|
the SSR stream (see `enterRenderPass`).
|
|
17
16
|
*/
|
|
18
17
|
// @documentation plumbing
|
|
19
|
-
export function mount(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const stop = scope(() => {
|
|
29
|
-
try {
|
|
30
|
-
build(host)
|
|
31
|
-
} finally {
|
|
32
|
-
exitRenderPass()
|
|
33
|
-
CURRENT_SCOPE.current = parentScope
|
|
34
|
-
}
|
|
35
|
-
})
|
|
18
|
+
export function mount(
|
|
19
|
+
host: Element,
|
|
20
|
+
build: (host: Element, props: unknown) => void,
|
|
21
|
+
props?: unknown,
|
|
22
|
+
): () => void {
|
|
23
|
+
/* Establish this component's lexical scope (nested, `awaiting` so it adopts the model
|
|
24
|
+
doc the build's first `doc()` creates) and render pass, run the build under it, and
|
|
25
|
+
restore the previous scope — the shared mount core (see `withScope`). */
|
|
26
|
+
const { stop, lexical } = withScope(scopeLabel(host), () => scope(() => build(host, props)))
|
|
36
27
|
return () => {
|
|
37
28
|
stop()
|
|
38
29
|
lexical.dispose()
|
|
@@ -3,32 +3,45 @@ import { hotReloadEnabled } from '../runtime/hotReloadEnabled.ts'
|
|
|
3
3
|
import { OWNER } from '../runtime/OWNER.ts'
|
|
4
4
|
import { registerHotInstance } from '../runtime/registerHotInstance.ts'
|
|
5
5
|
import type { UiComponent } from '../runtime/types/UiComponent.ts'
|
|
6
|
+
import { mountRange } from './mountRange.ts'
|
|
6
7
|
|
|
7
8
|
/*
|
|
8
|
-
Mounts a child component
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
mount
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
Mounts a child component as a marker-bounded range at `before` in `parent` — no
|
|
10
|
+
wrapper element, so the child's root is a true direct child of the parent (see
|
|
11
|
+
`mountRange`). Plain path (production, and dev without the hot bridge): just run the
|
|
12
|
+
range mount with the component's own `build`. Hot path (hotReloadEnabled and the
|
|
13
|
+
factory carries a module id): keep the mount handle (its range markers + disposer)
|
|
14
|
+
and record the instance so an edit can re-fill the same range in place (see
|
|
15
|
+
`hotReplace`), and file a cleanup with the mounting owner so the record and its scope
|
|
16
|
+
leave together when the parent (or branch/row) tears down.
|
|
16
17
|
*/
|
|
17
18
|
// @documentation plumbing
|
|
18
19
|
export function mountChild(
|
|
19
|
-
|
|
20
|
+
parent: Node,
|
|
20
21
|
factory: UiComponent,
|
|
21
22
|
props: Parameters<UiComponent>[1],
|
|
23
|
+
before: Node | null = null,
|
|
24
|
+
label?: string,
|
|
22
25
|
): void {
|
|
23
26
|
const moduleId = factory.__abideId
|
|
24
27
|
if (!hotReloadEnabled.current || moduleId === undefined) {
|
|
25
|
-
|
|
28
|
+
mountRange(parent, factory.build, props, before, label)
|
|
26
29
|
return
|
|
27
30
|
}
|
|
28
|
-
/* Capture the component's model alongside its
|
|
29
|
-
its state across (see `hotReplace`). */
|
|
30
|
-
const {
|
|
31
|
-
|
|
31
|
+
/* Capture the component's model alongside its mount handle, so a later swap can
|
|
32
|
+
carry its state across (see `hotReplace`). */
|
|
33
|
+
const { value: handle, model } = captureModelDoc(() =>
|
|
34
|
+
mountRange(parent, factory.build, props, before, label),
|
|
35
|
+
)
|
|
36
|
+
const instance = {
|
|
37
|
+
factory,
|
|
38
|
+
props,
|
|
39
|
+
label,
|
|
40
|
+
start: handle.start,
|
|
41
|
+
end: handle.end,
|
|
42
|
+
dispose: handle.dispose,
|
|
43
|
+
model,
|
|
44
|
+
}
|
|
32
45
|
const remove = registerHotInstance(moduleId, instance)
|
|
33
46
|
OWNER.current?.push(() => {
|
|
34
47
|
instance.dispose()
|