@vidact/runtime 0.2.0-beta.0 → 0.2.0-beta.5
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/CHANGELOG.md +52 -0
- package/README.md +1 -1
- package/dist/actions.d.ts +32 -30
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +251 -266
- package/dist/actions.js.map +1 -1
- package/dist/async.d.ts +20 -17
- package/dist/async.d.ts.map +1 -1
- package/dist/async.js +18 -17
- package/dist/async.js.map +1 -1
- package/dist/compiled/async-resource.d.ts +9 -6
- package/dist/compiled/async-resource.d.ts.map +1 -1
- package/dist/compiled/async-resource.js +69 -77
- package/dist/compiled/async-resource.js.map +1 -1
- package/dist/compiled/core.d.ts +136 -147
- package/dist/compiled/core.d.ts.map +1 -1
- package/dist/compiled/core.js +2672 -2985
- package/dist/compiled/core.js.map +1 -1
- package/dist/compiled/types.d.ts +44 -56
- package/dist/compiled/types.d.ts.map +1 -1
- package/dist/compiled/types.js +10 -6
- package/dist/compiled/types.js.map +1 -1
- package/dist/compiled/validation.d.ts +4 -1
- package/dist/compiled/validation.d.ts.map +1 -1
- package/dist/compiled/validation.js +17 -20
- package/dist/compiled/validation.js.map +1 -1
- package/dist/component-spread.d.ts +5 -3
- package/dist/component-spread.d.ts.map +1 -1
- package/dist/component-spread.js +49 -67
- package/dist/component-spread.js.map +1 -1
- package/dist/concurrent.d.ts +20 -18
- package/dist/concurrent.d.ts.map +1 -1
- package/dist/concurrent.js +191 -210
- package/dist/concurrent.js.map +1 -1
- package/dist/direct-dom.d.ts +17 -12
- package/dist/direct-dom.d.ts.map +1 -1
- package/dist/direct-dom.js +187 -221
- package/dist/direct-dom.js.map +1 -1
- package/dist/dom/attributes.d.ts +9 -6
- package/dist/dom/attributes.d.ts.map +1 -1
- package/dist/dom/attributes.js +65 -47
- package/dist/dom/attributes.js.map +1 -1
- package/dist/dom/events.d.ts +5 -2
- package/dist/dom/events.d.ts.map +1 -1
- package/dist/dom/events.js +53 -65
- package/dist/dom/events.js.map +1 -1
- package/dist/dom/forms.d.ts +9 -6
- package/dist/dom/forms.d.ts.map +1 -1
- package/dist/dom/forms.js +152 -198
- package/dist/dom/forms.js.map +1 -1
- package/dist/dom/intrinsic.d.ts +17 -15
- package/dist/dom/intrinsic.d.ts.map +1 -1
- package/dist/dom/intrinsic.js +48 -50
- package/dist/dom/intrinsic.js.map +1 -1
- package/dist/dom/namespace.d.ts +4 -1
- package/dist/dom/namespace.d.ts.map +1 -1
- package/dist/dom/namespace.js +25 -28
- package/dist/dom/namespace.js.map +1 -1
- package/dist/dom/namespaces.d.ts +11 -8
- package/dist/dom/namespaces.d.ts.map +1 -1
- package/dist/dom/namespaces.js +14 -12
- package/dist/dom/namespaces.js.map +1 -1
- package/dist/dom/properties.d.ts +18 -16
- package/dist/dom/properties.d.ts.map +1 -1
- package/dist/dom/properties.js +273 -296
- package/dist/dom/properties.js.map +1 -1
- package/dist/dom/style-properties.d.ts +4 -1
- package/dist/dom/style-properties.d.ts.map +1 -1
- package/dist/dom/style-properties.js +49 -45
- package/dist/dom/style-properties.js.map +1 -1
- package/dist/dom/styles.d.ts +5 -2
- package/dist/dom/styles.d.ts.map +1 -1
- package/dist/dom/styles.js +47 -67
- package/dist/dom/styles.js.map +1 -1
- package/dist/dom-forms.d.ts +4 -1
- package/dist/dom-forms.d.ts.map +1 -1
- package/dist/dom-forms.js +16 -13
- package/dist/dom-forms.js.map +1 -1
- package/dist/dom-styles.d.ts +4 -1
- package/dist/dom-styles.d.ts.map +1 -1
- package/dist/dom-styles.js +10 -7
- package/dist/dom-styles.js.map +1 -1
- package/dist/framework-hydrate.d.ts +20 -17
- package/dist/framework-hydrate.d.ts.map +1 -1
- package/dist/framework-hydrate.js +128 -146
- package/dist/framework-hydrate.js.map +1 -1
- package/dist/framework-protocol.d.ts +27 -25
- package/dist/framework-protocol.d.ts.map +1 -1
- package/dist/framework-protocol.js +173 -211
- package/dist/framework-protocol.js.map +1 -1
- package/dist/framework.d.ts +34 -32
- package/dist/framework.d.ts.map +1 -1
- package/dist/framework.js +74 -93
- package/dist/framework.js.map +1 -1
- package/dist/hydrate.d.ts +11 -8
- package/dist/hydrate.d.ts.map +1 -1
- package/dist/hydrate.js +13 -9
- package/dist/hydrate.js.map +1 -1
- package/dist/hydration-bridge.d.ts +70 -58
- package/dist/hydration-bridge.d.ts.map +1 -1
- package/dist/hydration-bridge.js +77 -66
- package/dist/hydration-bridge.js.map +1 -1
- package/dist/hydration.d.ts +54 -28
- package/dist/hydration.d.ts.map +1 -1
- package/dist/hydration.js +450 -463
- package/dist/hydration.js.map +1 -1
- package/dist/index.d.ts +13 -13
- package/dist/index.js +11 -13
- package/dist/indexed-list.d.ts +8 -5
- package/dist/indexed-list.d.ts.map +1 -1
- package/dist/indexed-list.js +14 -7
- package/dist/indexed-list.js.map +1 -1
- package/dist/jsx-runtime.d.ts +10 -8
- package/dist/jsx-runtime.d.ts.map +1 -1
- package/dist/jsx-runtime.js +28 -23
- package/dist/jsx-runtime.js.map +1 -1
- package/dist/keyed-list.d.ts +13 -14
- package/dist/keyed-list.d.ts.map +1 -1
- package/dist/keyed-list.js +132 -160
- package/dist/keyed-list.js.map +1 -1
- package/dist/metadata.d.ts +6 -3
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +131 -164
- package/dist/metadata.js.map +1 -1
- package/dist/nested-prop.d.ts +4 -2
- package/dist/nested-prop.d.ts.map +1 -1
- package/dist/nested-prop.js +17 -16
- package/dist/nested-prop.js.map +1 -1
- package/dist/profiling.d.ts +11 -8
- package/dist/profiling.d.ts.map +1 -1
- package/dist/profiling.js +17 -17
- package/dist/profiling.js.map +1 -1
- package/dist/protocol.d.ts +5 -2
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +8 -5
- package/dist/protocol.js.map +1 -1
- package/dist/raw-html.d.ts +5 -2
- package/dist/raw-html.d.ts.map +1 -1
- package/dist/raw-html.js +86 -146
- package/dist/raw-html.js.map +1 -1
- package/dist/ref.d.ts +8 -3
- package/dist/ref.d.ts.map +1 -1
- package/dist/ref.js +10 -2
- package/dist/ref.js.map +1 -1
- package/dist/renderable-protocol.d.ts +26 -8
- package/dist/renderable-protocol.d.ts.map +1 -1
- package/dist/renderable-protocol.js +30 -9
- package/dist/renderable-protocol.js.map +1 -1
- package/dist/renderable.d.ts +27 -22
- package/dist/renderable.d.ts.map +1 -1
- package/dist/renderable.js +141 -133
- package/dist/renderable.js.map +1 -1
- package/dist/retained-ui.d.ts +9 -6
- package/dist/retained-ui.d.ts.map +1 -1
- package/dist/retained-ui.js +12 -10
- package/dist/retained-ui.js.map +1 -1
- package/dist/root.d.ts +18 -15
- package/dist/root.d.ts.map +1 -1
- package/dist/root.js +73 -85
- package/dist/root.js.map +1 -1
- package/dist/scheduler.d.ts +10 -8
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +97 -105
- package/dist/scheduler.js.map +1 -1
- package/dist/server-framework.d.ts +45 -42
- package/dist/server-framework.d.ts.map +1 -1
- package/dist/server-framework.js +209 -217
- package/dist/server-framework.js.map +1 -1
- package/dist/server.d.ts +130 -128
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +603 -710
- package/dist/server.js.map +1 -1
- package/dist/shared/promise.d.ts +6 -3
- package/dist/shared/promise.d.ts.map +1 -1
- package/dist/shared/promise.js +21 -19
- package/dist/shared/promise.js.map +1 -1
- package/dist/source-mask.d.ts +9 -6
- package/dist/source-mask.d.ts.map +1 -1
- package/dist/source-mask.js +45 -63
- package/dist/source-mask.js.map +1 -1
- package/dist/spread.d.ts +5 -2
- package/dist/spread.d.ts.map +1 -1
- package/dist/spread.js +79 -99
- package/dist/spread.js.map +1 -1
- package/dist/state-slot.d.ts +18 -15
- package/dist/state-slot.d.ts.map +1 -1
- package/dist/state-slot.js +43 -42
- package/dist/state-slot.js.map +1 -1
- package/dist/testing.d.ts +9 -6
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +16 -12
- package/dist/testing.js.map +1 -1
- package/package.json +8 -7
- package/src/actions.ts +415 -0
- package/src/async.ts +78 -0
- package/src/compiled/async-resource.ts +113 -0
- package/src/compiled/core.ts +4041 -0
- package/src/compiled/types.ts +96 -0
- package/src/compiled/validation.ts +23 -0
- package/src/component-spread.ts +86 -0
- package/src/concurrent.ts +310 -0
- package/src/direct-dom.ts +365 -0
- package/src/dom/attributes.ts +55 -0
- package/src/dom/events.ts +93 -0
- package/src/dom/forms.ts +220 -0
- package/src/dom/intrinsic.ts +94 -0
- package/src/dom/namespace.ts +30 -0
- package/src/dom/namespaces.ts +14 -0
- package/src/dom/properties.ts +355 -0
- package/src/dom/style-properties.ts +46 -0
- package/src/dom/styles.ts +86 -0
- package/src/dom-forms.ts +23 -0
- package/src/dom-styles.ts +11 -0
- package/src/env.d.ts +6 -0
- package/src/framework-hydrate.ts +224 -0
- package/src/framework-protocol.ts +335 -0
- package/src/framework.ts +161 -0
- package/src/hydrate.ts +38 -0
- package/src/hydration-bridge.ts +196 -0
- package/src/hydration.ts +642 -0
- package/src/index.ts +92 -0
- package/src/indexed-list.ts +20 -0
- package/src/jsx-runtime.ts +55 -0
- package/src/keyed-list.ts +233 -0
- package/src/metadata.ts +212 -0
- package/src/nested-prop.ts +27 -0
- package/src/profiling.ts +43 -0
- package/src/protocol.ts +8 -0
- package/src/raw-html.ts +206 -0
- package/src/ref.ts +13 -0
- package/src/renderable-protocol.ts +63 -0
- package/src/renderable.ts +283 -0
- package/src/retained-ui.ts +19 -0
- package/src/root.ts +122 -0
- package/src/scheduler.ts +136 -0
- package/src/server-framework.ts +397 -0
- package/src/server.ts +1160 -0
- package/src/shared/promise.ts +28 -0
- package/src/source-mask.ts +83 -0
- package/src/spread.ts +131 -0
- package/src/state-slot.ts +76 -0
- package/src/testing.ts +27 -0
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
package/src/hydration.ts
ADDED
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HYDRATION_PREFIX,
|
|
3
|
+
HydrationMismatch,
|
|
4
|
+
installHydrationOperations,
|
|
5
|
+
type HydrationRange,
|
|
6
|
+
} from './hydration-bridge.ts'
|
|
7
|
+
|
|
8
|
+
interface HydrationState {
|
|
9
|
+
readonly host: ParentNode
|
|
10
|
+
readonly root: HydrationRange
|
|
11
|
+
readonly components: readonly HydrationRange[]
|
|
12
|
+
readonly claimedElements: WeakSet<Element>
|
|
13
|
+
readonly elementComponents: WeakMap<Element, Comment>
|
|
14
|
+
readonly elements: readonly Element[]
|
|
15
|
+
readonly cursors: WeakMap<Node, Node | null>
|
|
16
|
+
readonly slots: WeakMap<Node, Comment[]>
|
|
17
|
+
componentIndex: number
|
|
18
|
+
claimedElementCount: number
|
|
19
|
+
pendingStructuralParents: number
|
|
20
|
+
activeComponentStart: Comment | undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let activeHydration: HydrationState | undefined
|
|
24
|
+
let activeInsertionPoint: readonly [parent: Node, before: Node] | undefined
|
|
25
|
+
const hydrationFragments = new WeakMap<DocumentFragment, readonly unknown[]>()
|
|
26
|
+
|
|
27
|
+
export function beginHydration(host: ParentNode): () => void {
|
|
28
|
+
if (activeHydration !== undefined) {
|
|
29
|
+
throw new Error('a Vidact hydration is already active')
|
|
30
|
+
}
|
|
31
|
+
const ranges = scanRanges(host)
|
|
32
|
+
const root = ranges.roots[0]
|
|
33
|
+
if (root === undefined || ranges.roots.length !== 1) {
|
|
34
|
+
throw mismatch(`expected exactly one ${HYDRATION_PREFIX} root marker range`)
|
|
35
|
+
}
|
|
36
|
+
activeHydration = {
|
|
37
|
+
host,
|
|
38
|
+
root,
|
|
39
|
+
components: ranges.components,
|
|
40
|
+
claimedElements: new WeakSet(),
|
|
41
|
+
elementComponents: collectElementComponents(host),
|
|
42
|
+
elements: collectPostorderElements(root),
|
|
43
|
+
cursors: new WeakMap([[host as Node, root[0].nextSibling]]),
|
|
44
|
+
slots: new WeakMap(),
|
|
45
|
+
componentIndex: 0,
|
|
46
|
+
claimedElementCount: 0,
|
|
47
|
+
pendingStructuralParents: 0,
|
|
48
|
+
activeComponentStart: undefined,
|
|
49
|
+
}
|
|
50
|
+
return () => {
|
|
51
|
+
activeHydration = undefined
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function finishHydration(): void {
|
|
56
|
+
const state = requireHydration()
|
|
57
|
+
if (state.componentIndex !== state.components.length) {
|
|
58
|
+
throw mismatch(
|
|
59
|
+
`claimed ${state.componentIndex} of ${state.components.length} server component ranges`,
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
if (state.claimedElementCount !== state.elements.length) {
|
|
63
|
+
throw mismatch(
|
|
64
|
+
`claimed ${state.claimedElementCount} of ${state.elements.length} server elements`,
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
const rootCursor = cursor(state, state.host as Node)
|
|
68
|
+
if (rootCursor !== state.root[1]) {
|
|
69
|
+
throw mismatch(
|
|
70
|
+
`compiled root did not consume the complete server root range; stopped at ${JSON.stringify(markerValue(rootCursor as Node) ?? rootCursor?.nodeName ?? null)}`,
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function hydrationRootMarkers(): HydrationRange {
|
|
76
|
+
return requireHydration().root
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function isHydrating(): boolean {
|
|
80
|
+
return activeHydration !== undefined
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function createHydrationFragment(children: readonly unknown[]): DocumentFragment {
|
|
84
|
+
const fragment = document.createDocumentFragment()
|
|
85
|
+
hydrationFragments.set(fragment, children)
|
|
86
|
+
return fragment
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function hydrationFragmentChildren(
|
|
90
|
+
fragment: DocumentFragment,
|
|
91
|
+
): readonly unknown[] | undefined {
|
|
92
|
+
return hydrationFragments.get(fragment)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function isHydrationMismatch(error: unknown): error is HydrationMismatch {
|
|
96
|
+
return error instanceof HydrationMismatch
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function claimHydrationElement(
|
|
100
|
+
localName: string,
|
|
101
|
+
namespace: string | null,
|
|
102
|
+
matchesExpected?: (element: Element) => boolean,
|
|
103
|
+
): Element | undefined {
|
|
104
|
+
const state = activeHydration
|
|
105
|
+
if (state === undefined) return undefined
|
|
106
|
+
const candidates = state.elements.filter(
|
|
107
|
+
(candidate) =>
|
|
108
|
+
!state.claimedElements.has(candidate) &&
|
|
109
|
+
(state.activeComponentStart === undefined ||
|
|
110
|
+
state.elementComponents.get(candidate) === state.activeComponentStart) &&
|
|
111
|
+
candidate.localName === localName &&
|
|
112
|
+
candidate.namespaceURI === namespace,
|
|
113
|
+
)
|
|
114
|
+
const expectedMatches =
|
|
115
|
+
matchesExpected === undefined ? candidates : candidates.filter(matchesExpected)
|
|
116
|
+
const matches = expectedMatches.length === 0 ? candidates : expectedMatches
|
|
117
|
+
const element =
|
|
118
|
+
state.pendingStructuralParents > 0
|
|
119
|
+
? (matches.find(containsArrayMarker) ?? matches[0])
|
|
120
|
+
: matches[0]
|
|
121
|
+
if (element === undefined) {
|
|
122
|
+
throw mismatch(
|
|
123
|
+
state.pendingStructuralParents > 0
|
|
124
|
+
? `missing server structural parent <${localName}>`
|
|
125
|
+
: `missing server element <${localName}>`,
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
if (state.pendingStructuralParents > 0) state.pendingStructuralParents = 0
|
|
129
|
+
state.claimedElements.add(element)
|
|
130
|
+
state.claimedElementCount += 1
|
|
131
|
+
state.cursors.set(element, initialCursor(element))
|
|
132
|
+
return element
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Called by a compiled list right before its container element is claimed. Lists are the
|
|
137
|
+
* only structural bindings whose server markup carries an `a` marker, so they are the
|
|
138
|
+
* only ones that may prefer a candidate containing one — for a conditional, Suspense or
|
|
139
|
+
* any other structural child that bias would pick an unrelated element that happens to
|
|
140
|
+
* hold a list (see `claimHydrationElement`).
|
|
141
|
+
*/
|
|
142
|
+
export function noteHydrationStructuralParent(): void {
|
|
143
|
+
if (activeHydration !== undefined) activeHydration.pendingStructuralParents += 1
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function claimHydrationComponentRange(): HydrationRange | undefined {
|
|
147
|
+
const state = activeHydration
|
|
148
|
+
if (state === undefined) return undefined
|
|
149
|
+
const range = state.components[state.componentIndex]
|
|
150
|
+
if (range === undefined) throw mismatch('missing server component marker range')
|
|
151
|
+
state.componentIndex += 1
|
|
152
|
+
const parent = range[0].parentNode
|
|
153
|
+
if (parent === null || range[1].parentNode !== parent) {
|
|
154
|
+
throw mismatch('server component marker range is detached')
|
|
155
|
+
}
|
|
156
|
+
return range
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function withHydrationComponentRange<Result>(
|
|
160
|
+
range: HydrationRange,
|
|
161
|
+
operation: () => Result,
|
|
162
|
+
): Result {
|
|
163
|
+
const state = activeHydration
|
|
164
|
+
if (state === undefined) return operation()
|
|
165
|
+
const previous = state.activeComponentStart
|
|
166
|
+
state.activeComponentStart = range[0]
|
|
167
|
+
try {
|
|
168
|
+
return operation()
|
|
169
|
+
} finally {
|
|
170
|
+
state.activeComponentStart = previous
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function withHydrationCursor<Result>(
|
|
175
|
+
parent: Node,
|
|
176
|
+
value: Node | null,
|
|
177
|
+
operation: () => Result,
|
|
178
|
+
): Result {
|
|
179
|
+
const state = activeHydration
|
|
180
|
+
if (state === undefined) return operation()
|
|
181
|
+
const hadCursor = state.cursors.has(parent)
|
|
182
|
+
const previous = state.cursors.get(parent) ?? null
|
|
183
|
+
state.cursors.set(parent, value)
|
|
184
|
+
try {
|
|
185
|
+
return operation()
|
|
186
|
+
} finally {
|
|
187
|
+
if (hadCursor) state.cursors.set(parent, previous)
|
|
188
|
+
else state.cursors.delete(parent)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function claimHydrationNode(parent: Node, node: Node): boolean {
|
|
193
|
+
const state = activeHydration
|
|
194
|
+
if (state === undefined) return false
|
|
195
|
+
enterHydrationSlot(state, parent)
|
|
196
|
+
const next = cursor(state, parent)
|
|
197
|
+
if (next !== node) {
|
|
198
|
+
throw mismatch(`expected existing ${node.nodeName} at the current hydration position`)
|
|
199
|
+
}
|
|
200
|
+
setHydrationCursor(state, parent, node.nextSibling)
|
|
201
|
+
return true
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function claimHydrationComponentMount(parent: Node, start: Comment, end: Comment): boolean {
|
|
205
|
+
const state = activeHydration
|
|
206
|
+
if (state === undefined) return false
|
|
207
|
+
enterHydrationSlot(state, parent)
|
|
208
|
+
const current = cursor(state, parent)
|
|
209
|
+
if (
|
|
210
|
+
(current !== start && current !== end) ||
|
|
211
|
+
start.parentNode !== parent ||
|
|
212
|
+
end.parentNode !== parent
|
|
213
|
+
) {
|
|
214
|
+
throw mismatch('server component range does not match the compiled child position')
|
|
215
|
+
}
|
|
216
|
+
setHydrationCursor(state, parent, end.nextSibling)
|
|
217
|
+
return true
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function claimHydrationText(parent: Node, expected: string): Text | null | undefined {
|
|
221
|
+
const state = activeHydration
|
|
222
|
+
if (state === undefined) return undefined
|
|
223
|
+
enterHydrationSlot(state, parent)
|
|
224
|
+
return claimTextPrefix(state, parent, expected)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Claims the text for a scalar binding together with a range it can later swap an
|
|
229
|
+
* element or collection into. The server emits no markers around text, so the binding
|
|
230
|
+
* borrows the enclosing child-slot markers when the slot holds exactly this text — the
|
|
231
|
+
* common case, and no DOM is touched. A text node shared with adjacent scalars (one
|
|
232
|
+
* parsed node for several array items) gets fresh anchor comments instead.
|
|
233
|
+
*/
|
|
234
|
+
export function claimHydrationTextRange(
|
|
235
|
+
parent: Node,
|
|
236
|
+
expected: string,
|
|
237
|
+
): readonly [start: Comment, end: Comment, text: Text | null] | undefined {
|
|
238
|
+
const state = activeHydration
|
|
239
|
+
if (state === undefined) return undefined
|
|
240
|
+
enterHydrationSlot(state, parent)
|
|
241
|
+
const at = cursor(state, parent)
|
|
242
|
+
const slotStart = at === null ? parent.lastChild : at.previousSibling
|
|
243
|
+
const text = claimTextPrefix(state, parent, expected)
|
|
244
|
+
const slotEnd = text === null ? at : text.nextSibling
|
|
245
|
+
if (
|
|
246
|
+
isMarker(slotStart, `${HYDRATION_PREFIX}:b`) &&
|
|
247
|
+
isMarker(slotEnd, `/${HYDRATION_PREFIX}:b`) &&
|
|
248
|
+
findClosingSibling(slotStart, `${HYDRATION_PREFIX}:b`) === slotEnd
|
|
249
|
+
) {
|
|
250
|
+
return [slotStart, slotEnd, text]
|
|
251
|
+
}
|
|
252
|
+
const start = document.createComment('')
|
|
253
|
+
const end = document.createComment('')
|
|
254
|
+
const reference = text ?? at
|
|
255
|
+
parent.insertBefore(start, reference)
|
|
256
|
+
parent.insertBefore(end, text === null ? reference : text.nextSibling)
|
|
257
|
+
return [start, end, text]
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* The server renders a scalar as bare text, and nothing at all for an empty one. Adjacent
|
|
262
|
+
* scalars in one collection parse into a single text node, so a claim takes only its own
|
|
263
|
+
* prefix and splits the rest off for the next claim.
|
|
264
|
+
*/
|
|
265
|
+
function claimTextPrefix(state: HydrationState, parent: Node, expected: string): Text | null {
|
|
266
|
+
const node = cursor(state, parent)
|
|
267
|
+
if (expected === '') {
|
|
268
|
+
if (node instanceof Text && node.data === '') {
|
|
269
|
+
setHydrationCursor(state, parent, node.nextSibling)
|
|
270
|
+
return node
|
|
271
|
+
}
|
|
272
|
+
// Nothing to consume, but closing slot markers at the cursor still need passing.
|
|
273
|
+
setHydrationCursor(state, parent, node)
|
|
274
|
+
return null
|
|
275
|
+
}
|
|
276
|
+
if (!(node instanceof Text)) {
|
|
277
|
+
throw mismatch(
|
|
278
|
+
`expected server text ${JSON.stringify(expected)} in ${describeNode(parent)}; found ${describeNode(node)}`,
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
if (!node.data.startsWith(expected)) {
|
|
282
|
+
throw mismatch(
|
|
283
|
+
`server text ${JSON.stringify(node.data)} does not match ${JSON.stringify(expected)}`,
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
if (node.data.length > expected.length) node.splitText(expected.length)
|
|
287
|
+
setHydrationCursor(state, parent, node.nextSibling)
|
|
288
|
+
return node
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function claimHydrationArrayRange(parent: Node): HydrationRange | undefined {
|
|
292
|
+
const state = activeHydration
|
|
293
|
+
if (state === undefined) return undefined
|
|
294
|
+
enterHydrationSlot(state, parent)
|
|
295
|
+
const start = cursor(state, parent)
|
|
296
|
+
if (!isMarker(start, `${HYDRATION_PREFIX}:a`)) {
|
|
297
|
+
throw mismatch(
|
|
298
|
+
`expected a ${HYDRATION_PREFIX} array marker in ${describeNode(parent)}; found ${JSON.stringify(markerValue(start as Node) ?? start?.nodeName ?? null)}`,
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
const end = findClosingSibling(start, `${HYDRATION_PREFIX}:a`)
|
|
302
|
+
state.cursors.set(parent, start.nextSibling)
|
|
303
|
+
return [start, end]
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function finishHydrationArrayRange(parent: Node, end: Comment): void {
|
|
307
|
+
const state = requireHydration()
|
|
308
|
+
if (cursor(state, parent) !== end) {
|
|
309
|
+
throw mismatch('compiled collection did not consume its complete server array range')
|
|
310
|
+
}
|
|
311
|
+
setHydrationCursor(state, parent, end.nextSibling)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function claimHydrationSlotRange(parent: Node): HydrationRange | undefined {
|
|
315
|
+
const state = activeHydration
|
|
316
|
+
if (state === undefined) return undefined
|
|
317
|
+
const start = cursor(state, parent)
|
|
318
|
+
if (!isMarker(start, `${HYDRATION_PREFIX}:b`)) {
|
|
319
|
+
throw mismatch(
|
|
320
|
+
`expected a ${HYDRATION_PREFIX} child-slot marker in ${describeNode(parent)}; found ${JSON.stringify(markerValue(start as Node) ?? start?.nodeName ?? null)} after ${JSON.stringify(markerValue(start?.previousSibling as Node) ?? start?.previousSibling?.nodeName ?? null)}; insertion=${JSON.stringify(markerValue(activeInsertionPoint?.[1] as Node) ?? activeInsertionPoint?.[1]?.nodeName ?? null)}`,
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
const end = findClosingSibling(start, `${HYDRATION_PREFIX}:b`)
|
|
324
|
+
const slots = state.slots.get(parent) ?? []
|
|
325
|
+
slots.push(end)
|
|
326
|
+
state.slots.set(parent, slots)
|
|
327
|
+
state.cursors.set(parent, start.nextSibling)
|
|
328
|
+
return [start, end]
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function borrowHydrationSlotRange(
|
|
332
|
+
parent: Node,
|
|
333
|
+
current: boolean,
|
|
334
|
+
): HydrationRange | undefined {
|
|
335
|
+
const state = activeHydration
|
|
336
|
+
if (state === undefined) return undefined
|
|
337
|
+
const start = (
|
|
338
|
+
current ? cursor(state, parent) : cursor(state, parent)?.previousSibling
|
|
339
|
+
) as Node | null
|
|
340
|
+
if (!isMarker(start, `${HYDRATION_PREFIX}:b`)) return undefined
|
|
341
|
+
if (
|
|
342
|
+
!current &&
|
|
343
|
+
activeInsertionPoint?.[0] === parent &&
|
|
344
|
+
isMarker(activeInsertionPoint[1], `/${HYDRATION_PREFIX}:b`)
|
|
345
|
+
) {
|
|
346
|
+
return [start, activeInsertionPoint[1]]
|
|
347
|
+
}
|
|
348
|
+
return [start, findClosingSibling(start, `${HYDRATION_PREFIX}:b`)]
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Claims everything between two markers as-is, without hydrating into it. A Suspense
|
|
353
|
+
* boundary uses this when the server rendered its content but the client cannot
|
|
354
|
+
* produce it yet (a lazy chunk still loading): the server DOM stays on screen as a
|
|
355
|
+
* dehydrated boundary and the client render replaces it once the resource resolves.
|
|
356
|
+
* The elements and component ranges inside are marked claimed so `finishHydration`'s
|
|
357
|
+
* accounting still balances.
|
|
358
|
+
*/
|
|
359
|
+
export function skipHydrationRange(start: Comment, end: Comment): void {
|
|
360
|
+
const state = requireHydration()
|
|
361
|
+
const parent = start.parentNode
|
|
362
|
+
if (parent === null || end.parentNode !== parent) throw mismatch('detached server marker range')
|
|
363
|
+
for (const element of collectPostorderElements([start, end])) {
|
|
364
|
+
if (state.claimedElements.has(element)) continue
|
|
365
|
+
state.claimedElements.add(element)
|
|
366
|
+
state.claimedElementCount += 1
|
|
367
|
+
}
|
|
368
|
+
// Component ranges are sorted by document position and every earlier one is already
|
|
369
|
+
// claimed, so the ones inside this range sit contiguously at the current index.
|
|
370
|
+
while (state.componentIndex < state.components.length) {
|
|
371
|
+
const componentStart = state.components[state.componentIndex]![0]
|
|
372
|
+
const inside =
|
|
373
|
+
(start.compareDocumentPosition(componentStart) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0 &&
|
|
374
|
+
(componentStart.compareDocumentPosition(end) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0
|
|
375
|
+
if (!inside) break
|
|
376
|
+
state.componentIndex += 1
|
|
377
|
+
}
|
|
378
|
+
state.pendingStructuralParents = 0
|
|
379
|
+
setHydrationCursor(state, parent, end)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function claimHydrationSuspenseFallback(parent: Node): Comment | undefined {
|
|
383
|
+
const state = activeHydration
|
|
384
|
+
if (state === undefined) return undefined
|
|
385
|
+
const marker = cursor(state, parent)
|
|
386
|
+
if (!isMarker(marker, `${HYDRATION_PREFIX}:p`)) return undefined
|
|
387
|
+
setHydrationCursor(state, parent, marker.nextSibling)
|
|
388
|
+
return marker
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function withoutHydration<Result>(operation: () => Result): Result {
|
|
392
|
+
const previous = activeHydration
|
|
393
|
+
activeHydration = undefined
|
|
394
|
+
try {
|
|
395
|
+
return operation()
|
|
396
|
+
} finally {
|
|
397
|
+
activeHydration = previous
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function withHydrationInsertion<Result>(
|
|
402
|
+
parent: Node,
|
|
403
|
+
before: Node,
|
|
404
|
+
operation: () => Result,
|
|
405
|
+
): Result {
|
|
406
|
+
if (activeHydration === undefined) return operation()
|
|
407
|
+
const previous = activeInsertionPoint
|
|
408
|
+
activeInsertionPoint = [parent, before]
|
|
409
|
+
try {
|
|
410
|
+
return operation()
|
|
411
|
+
} finally {
|
|
412
|
+
activeInsertionPoint = previous
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export function hydrationInsertionPoint(): readonly [parent: Node, before: Node] | undefined {
|
|
417
|
+
return activeInsertionPoint
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export function hydrationCursor(parent: Node): Node | null | undefined {
|
|
421
|
+
const state = activeHydration
|
|
422
|
+
return state === undefined ? undefined : cursor(state, parent)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export function hydrationRangeParent(start: Comment, end: Comment): Node | undefined {
|
|
426
|
+
if (activeHydration === undefined) return undefined
|
|
427
|
+
const parent = start.parentNode
|
|
428
|
+
if (parent === null || end.parentNode !== parent) throw mismatch('detached server marker range')
|
|
429
|
+
return parent
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function requireHydration(): HydrationState {
|
|
433
|
+
if (activeHydration === undefined) throw new Error('no Vidact hydration is active')
|
|
434
|
+
return activeHydration
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function cursor(state: HydrationState, parent: Node): Node | null {
|
|
438
|
+
if (!state.cursors.has(parent)) state.cursors.set(parent, initialCursor(parent))
|
|
439
|
+
return state.cursors.get(parent) ?? null
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function initialCursor(parent: Node): Node | null {
|
|
443
|
+
return parent.firstChild
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function enterHydrationSlot(state: HydrationState, parent: Node): void {
|
|
447
|
+
const slots = state.slots.get(parent) ?? []
|
|
448
|
+
let start = cursor(state, parent)
|
|
449
|
+
while (isMarker(start, `${HYDRATION_PREFIX}:b`)) {
|
|
450
|
+
slots.push(findClosingSibling(start, `${HYDRATION_PREFIX}:b`))
|
|
451
|
+
start = start.nextSibling
|
|
452
|
+
}
|
|
453
|
+
if (slots.length !== 0) state.slots.set(parent, slots)
|
|
454
|
+
state.cursors.set(parent, start)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function setHydrationCursor(state: HydrationState, parent: Node, value: Node | null): void {
|
|
458
|
+
const slots = state.slots.get(parent)
|
|
459
|
+
let next = value
|
|
460
|
+
if (slots !== undefined) {
|
|
461
|
+
for (;;) {
|
|
462
|
+
const end = slots.at(-1)
|
|
463
|
+
if (end !== next) break
|
|
464
|
+
slots.pop()
|
|
465
|
+
next = end.nextSibling
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
state.cursors.set(parent, next)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function mismatch(message: string): HydrationMismatch {
|
|
472
|
+
return new HydrationMismatch(message)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** `DIV.class-a[data-x]` — enough of an element to find it in the server markup. */
|
|
476
|
+
function describeNode(node: Node | null | undefined): string {
|
|
477
|
+
if (!(node instanceof Element)) return node?.nodeName ?? 'null'
|
|
478
|
+
const classes = node.classList.length === 0 ? '' : `.${[...node.classList].join('.')}`
|
|
479
|
+
const data = [...node.attributes]
|
|
480
|
+
.filter((attribute) => attribute.name.startsWith('data-') || attribute.name === 'id')
|
|
481
|
+
.map((attribute) => `[${attribute.name}]`)
|
|
482
|
+
.join('')
|
|
483
|
+
return `${node.nodeName}${classes}${data}`
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function markerValue(node: Node): string | undefined {
|
|
487
|
+
return node instanceof Comment ? node.data : undefined
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function isMarker(node: Node | null, value: string): node is Comment {
|
|
491
|
+
return markerValue(node as Node) === value
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function findClosingSibling(start: Comment, kind: string): Comment {
|
|
495
|
+
let depth = 0
|
|
496
|
+
for (let node = start.nextSibling; node !== null; node = node.nextSibling) {
|
|
497
|
+
if (isMarker(node, kind)) {
|
|
498
|
+
depth += 1
|
|
499
|
+
continue
|
|
500
|
+
}
|
|
501
|
+
if (!isMarker(node, `/${kind}`)) continue
|
|
502
|
+
if (depth === 0) return node
|
|
503
|
+
depth -= 1
|
|
504
|
+
}
|
|
505
|
+
throw mismatch(`missing closing ${kind} marker`)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/** Paired markers; `p` (pending fallback) is a lone marker and stays out of the stacks. */
|
|
509
|
+
const RANGE_MARKER = new RegExp(`^\\/?${HYDRATION_PREFIX}:([abchr])$`)
|
|
510
|
+
|
|
511
|
+
function scanRanges(host: ParentNode): {
|
|
512
|
+
roots: HydrationRange[]
|
|
513
|
+
components: HydrationRange[]
|
|
514
|
+
} {
|
|
515
|
+
const roots: HydrationRange[] = []
|
|
516
|
+
const components: HydrationRange[] = []
|
|
517
|
+
const stacks = new Map<string, Comment[]>()
|
|
518
|
+
const walker = document.createTreeWalker(host, NodeFilter.SHOW_COMMENT)
|
|
519
|
+
for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {
|
|
520
|
+
const comment = node as Comment
|
|
521
|
+
const match = RANGE_MARKER.exec(comment.data)
|
|
522
|
+
if (match === null) continue
|
|
523
|
+
const kind = match[1] as 'a' | 'b' | 'c' | 'h' | 'r'
|
|
524
|
+
if (!comment.data.startsWith('/')) {
|
|
525
|
+
const stack = stacks.get(kind) ?? []
|
|
526
|
+
stack.push(comment)
|
|
527
|
+
stacks.set(kind, stack)
|
|
528
|
+
continue
|
|
529
|
+
}
|
|
530
|
+
const start = stacks.get(kind)?.pop()
|
|
531
|
+
if (start === undefined) throw mismatch(`orphan closing ${HYDRATION_PREFIX}:${kind} marker`)
|
|
532
|
+
if (kind === 'r') roots.push([start, comment])
|
|
533
|
+
if (kind === 'c') components.push([start, comment])
|
|
534
|
+
}
|
|
535
|
+
if ([...stacks.values()].some((stack) => stack.length !== 0)) {
|
|
536
|
+
throw mismatch(`unclosed ${HYDRATION_PREFIX} hydration marker`)
|
|
537
|
+
}
|
|
538
|
+
components.sort(([left], [right]) => {
|
|
539
|
+
const position = left.compareDocumentPosition(right)
|
|
540
|
+
if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1
|
|
541
|
+
if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1
|
|
542
|
+
return 0
|
|
543
|
+
})
|
|
544
|
+
return { roots, components }
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function collectPostorderElements(root: HydrationRange): Element[] {
|
|
548
|
+
const elements: Element[] = []
|
|
549
|
+
const visit = (element: Element): void => {
|
|
550
|
+
visitChildren(element)
|
|
551
|
+
elements.push(element)
|
|
552
|
+
}
|
|
553
|
+
const visitChildren = (parent: Node): void => {
|
|
554
|
+
let opaqueDepth = 0
|
|
555
|
+
for (const child of parent.childNodes) {
|
|
556
|
+
if (isMarker(child, `${HYDRATION_PREFIX}:h`)) {
|
|
557
|
+
opaqueDepth += 1
|
|
558
|
+
continue
|
|
559
|
+
}
|
|
560
|
+
if (isMarker(child, `/${HYDRATION_PREFIX}:h`)) {
|
|
561
|
+
opaqueDepth -= 1
|
|
562
|
+
continue
|
|
563
|
+
}
|
|
564
|
+
if (opaqueDepth === 0 && child instanceof Element) visit(child)
|
|
565
|
+
}
|
|
566
|
+
if (opaqueDepth !== 0) throw mismatch(`unbalanced ${HYDRATION_PREFIX} raw HTML marker range`)
|
|
567
|
+
}
|
|
568
|
+
let opaqueDepth = 0
|
|
569
|
+
for (let node = root[0].nextSibling; node !== null && node !== root[1]; node = node.nextSibling) {
|
|
570
|
+
if (isMarker(node, `${HYDRATION_PREFIX}:h`)) {
|
|
571
|
+
opaqueDepth += 1
|
|
572
|
+
continue
|
|
573
|
+
}
|
|
574
|
+
if (isMarker(node, `/${HYDRATION_PREFIX}:h`)) {
|
|
575
|
+
opaqueDepth -= 1
|
|
576
|
+
continue
|
|
577
|
+
}
|
|
578
|
+
if (opaqueDepth === 0 && node instanceof Element) visit(node)
|
|
579
|
+
}
|
|
580
|
+
if (opaqueDepth !== 0) throw mismatch(`unbalanced ${HYDRATION_PREFIX} raw HTML marker range`)
|
|
581
|
+
return elements
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function collectElementComponents(host: ParentNode): WeakMap<Element, Comment> {
|
|
585
|
+
const owners = new WeakMap<Element, Comment>()
|
|
586
|
+
const components: Comment[] = []
|
|
587
|
+
const walker = document.createTreeWalker(host, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT)
|
|
588
|
+
for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {
|
|
589
|
+
if (isMarker(node, `${HYDRATION_PREFIX}:c`)) {
|
|
590
|
+
components.push(node)
|
|
591
|
+
continue
|
|
592
|
+
}
|
|
593
|
+
if (isMarker(node, `/${HYDRATION_PREFIX}:c`)) {
|
|
594
|
+
components.pop()
|
|
595
|
+
continue
|
|
596
|
+
}
|
|
597
|
+
if (node instanceof Element) {
|
|
598
|
+
const owner = components.at(-1)
|
|
599
|
+
if (owner !== undefined) owners.set(node, owner)
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return owners
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function containsArrayMarker(element: Element): boolean {
|
|
606
|
+
const walker = document.createTreeWalker(element, NodeFilter.SHOW_COMMENT)
|
|
607
|
+
for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {
|
|
608
|
+
if (isMarker(node, `${HYDRATION_PREFIX}:a`)) return true
|
|
609
|
+
}
|
|
610
|
+
return false
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
export function installHydration(): void {
|
|
614
|
+
installHydrationOperations({
|
|
615
|
+
begin: beginHydration,
|
|
616
|
+
finish: finishHydration,
|
|
617
|
+
rootMarkers: hydrationRootMarkers,
|
|
618
|
+
active: isHydrating,
|
|
619
|
+
createFragment: createHydrationFragment,
|
|
620
|
+
fragmentChildren: hydrationFragmentChildren,
|
|
621
|
+
claimElement: claimHydrationElement,
|
|
622
|
+
noteStructuralParent: noteHydrationStructuralParent,
|
|
623
|
+
claimComponentRange: claimHydrationComponentRange,
|
|
624
|
+
withComponentRange: withHydrationComponentRange,
|
|
625
|
+
withCursor: withHydrationCursor,
|
|
626
|
+
claimNode: claimHydrationNode,
|
|
627
|
+
claimComponentMount: claimHydrationComponentMount,
|
|
628
|
+
claimText: claimHydrationText,
|
|
629
|
+
claimTextRange: claimHydrationTextRange,
|
|
630
|
+
claimArrayRange: claimHydrationArrayRange,
|
|
631
|
+
finishArrayRange: finishHydrationArrayRange,
|
|
632
|
+
claimSlotRange: claimHydrationSlotRange,
|
|
633
|
+
borrowSlotRange: borrowHydrationSlotRange,
|
|
634
|
+
claimSuspenseFallback: claimHydrationSuspenseFallback,
|
|
635
|
+
skipRange: skipHydrationRange,
|
|
636
|
+
withoutHydration,
|
|
637
|
+
withInsertion: withHydrationInsertion,
|
|
638
|
+
insertionPoint: hydrationInsertionPoint,
|
|
639
|
+
cursor: hydrationCursor,
|
|
640
|
+
rangeParent: hydrationRangeParent,
|
|
641
|
+
})
|
|
642
|
+
}
|