@remix-run/ui 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +195 -2
- package/dist/animation/animate-layout-mixin.d.ts +12 -0
- package/dist/animation/animate-layout-mixin.js +214 -0
- package/dist/animation/animate-layout-mixin.js.map +1 -0
- package/dist/animation/animate-mixins.d.ts +29 -0
- package/dist/animation/animate-mixins.js +235 -0
- package/dist/animation/animate-mixins.js.map +1 -0
- package/dist/animation/index.d.ts +6 -0
- package/dist/animation/index.js +5 -0
- package/dist/animation/index.js.map +1 -0
- package/dist/animation/spring.d.ts +75 -0
- package/dist/animation/spring.js +183 -0
- package/dist/animation/spring.js.map +1 -0
- package/dist/animation/tween.d.ts +70 -0
- package/dist/animation/tween.js +93 -0
- package/dist/animation/tween.js.map +1 -0
- package/dist/components/accordion/accordion.d.ts +92 -0
- package/dist/components/accordion/accordion.js +337 -0
- package/dist/components/accordion/accordion.js.map +1 -0
- package/dist/components/anchor/anchor.d.ts +13 -0
- package/dist/components/anchor/anchor.js +558 -0
- package/dist/components/anchor/anchor.js.map +1 -0
- package/dist/components/breadcrumbs/breadcrumbs.d.ts +11 -0
- package/dist/components/breadcrumbs/breadcrumbs.js +78 -0
- package/dist/components/breadcrumbs/breadcrumbs.js.map +1 -0
- package/dist/components/button/button.d.ts +23 -0
- package/dist/components/button/button.js +147 -0
- package/dist/components/button/button.js.map +1 -0
- package/dist/components/combobox/combobox.d.ts +101 -0
- package/dist/components/combobox/combobox.js +708 -0
- package/dist/components/combobox/combobox.js.map +1 -0
- package/dist/components/glyph/glyph.d.ts +14 -0
- package/dist/components/glyph/glyph.js +65 -0
- package/dist/components/glyph/glyph.js.map +1 -0
- package/dist/components/listbox/listbox.d.ts +67 -0
- package/dist/components/listbox/listbox.js +340 -0
- package/dist/components/listbox/listbox.js.map +1 -0
- package/dist/components/menu/hover-aim.d.ts +5 -0
- package/dist/components/menu/hover-aim.js +308 -0
- package/dist/components/menu/hover-aim.js.map +1 -0
- package/dist/components/menu/menu.d.ts +164 -0
- package/dist/components/menu/menu.js +1106 -0
- package/dist/components/menu/menu.js.map +1 -0
- package/dist/components/popover/popover.d.ts +35 -0
- package/dist/components/popover/popover.js +138 -0
- package/dist/components/popover/popover.js.map +1 -0
- package/dist/components/select/select.d.ts +67 -0
- package/dist/components/select/select.js +397 -0
- package/dist/components/select/select.js.map +1 -0
- package/dist/components/separator/separator.d.ts +2 -0
- package/dist/components/separator/separator.js +15 -0
- package/dist/components/separator/separator.js.map +1 -0
- package/dist/components/tabs/tabs.d.ts +78 -0
- package/dist/components/tabs/tabs.js +341 -0
- package/dist/components/tabs/tabs.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/interactions/keydown/keydown.d.ts +1 -0
- package/dist/interactions/keydown/keydown.js +8 -0
- package/dist/interactions/keydown/keydown.js.map +1 -0
- package/dist/interactions/outside-click/outside-click-mixin.d.ts +2 -0
- package/dist/interactions/outside-click/outside-click-mixin.js +29 -0
- package/dist/interactions/outside-click/outside-click-mixin.js.map +1 -0
- package/dist/interactions/typeahead/typeahead-mixin.d.ts +8 -0
- package/dist/interactions/typeahead/typeahead-mixin.js +59 -0
- package/dist/interactions/typeahead/typeahead-mixin.js.map +1 -0
- package/dist/jsx-runtime.d.ts +2 -0
- package/dist/jsx-runtime.js +3 -0
- package/dist/jsx-runtime.js.map +1 -0
- package/dist/runtime/client-entries.d.ts +91 -0
- package/dist/runtime/client-entries.js +40 -0
- package/dist/runtime/client-entries.js.map +1 -0
- package/dist/runtime/component.d.ts +223 -0
- package/dist/runtime/component.js +152 -0
- package/dist/runtime/component.js.map +1 -0
- package/dist/runtime/create-element.d.ts +10 -0
- package/dist/runtime/create-element.js +29 -0
- package/dist/runtime/create-element.js.map +1 -0
- package/dist/runtime/diff-dom.d.ts +2 -0
- package/dist/runtime/diff-dom.js +364 -0
- package/dist/runtime/diff-dom.js.map +1 -0
- package/dist/runtime/diff-props.d.ts +9 -0
- package/dist/runtime/diff-props.js +195 -0
- package/dist/runtime/diff-props.js.map +1 -0
- package/dist/runtime/document-state.d.ts +11 -0
- package/dist/runtime/document-state.js +106 -0
- package/dist/runtime/document-state.js.map +1 -0
- package/dist/runtime/dom.d.ts +3189 -0
- package/dist/runtime/dom.js +2 -0
- package/dist/runtime/dom.js.map +1 -0
- package/dist/runtime/error-event.d.ts +20 -0
- package/dist/runtime/error-event.js +19 -0
- package/dist/runtime/error-event.js.map +1 -0
- package/dist/runtime/event-listeners.d.ts +50 -0
- package/dist/runtime/event-listeners.js +31 -0
- package/dist/runtime/event-listeners.js.map +1 -0
- package/dist/runtime/frame.d.ts +119 -0
- package/dist/runtime/frame.js +915 -0
- package/dist/runtime/frame.js.map +1 -0
- package/dist/runtime/invariant.d.ts +23 -0
- package/dist/runtime/invariant.js +33 -0
- package/dist/runtime/invariant.js.map +1 -0
- package/dist/runtime/jsx.d.ts +320 -0
- package/dist/runtime/jsx.js +31 -0
- package/dist/runtime/jsx.js.map +1 -0
- package/dist/runtime/mixins/attrs-mixin.d.ts +9 -0
- package/dist/runtime/mixins/attrs-mixin.js +23 -0
- package/dist/runtime/mixins/attrs-mixin.js.map +1 -0
- package/dist/runtime/mixins/link-mixin.d.ts +17 -0
- package/dist/runtime/mixins/link-mixin.js +101 -0
- package/dist/runtime/mixins/link-mixin.js.map +1 -0
- package/dist/runtime/mixins/mixin.d.ts +145 -0
- package/dist/runtime/mixins/mixin.js +570 -0
- package/dist/runtime/mixins/mixin.js.map +1 -0
- package/dist/runtime/mixins/on-mixin.d.ts +16 -0
- package/dist/runtime/mixins/on-mixin.js +49 -0
- package/dist/runtime/mixins/on-mixin.js.map +1 -0
- package/dist/runtime/mixins/ref-mixin.d.ts +9 -0
- package/dist/runtime/mixins/ref-mixin.js +21 -0
- package/dist/runtime/mixins/ref-mixin.js.map +1 -0
- package/dist/runtime/navigation.d.ts +22 -0
- package/dist/runtime/navigation.js +102 -0
- package/dist/runtime/navigation.js.map +1 -0
- package/dist/runtime/reconcile.d.ts +11 -0
- package/dist/runtime/reconcile.js +1240 -0
- package/dist/runtime/reconcile.js.map +1 -0
- package/dist/runtime/render.d.ts +54 -0
- package/dist/runtime/render.js +50 -0
- package/dist/runtime/render.js.map +1 -0
- package/dist/runtime/run.d.ts +45 -0
- package/dist/runtime/run.js +68 -0
- package/dist/runtime/run.js.map +1 -0
- package/dist/runtime/scheduler.d.ts +30 -0
- package/dist/runtime/scheduler.js +185 -0
- package/dist/runtime/scheduler.js.map +1 -0
- package/dist/runtime/svg-attributes.d.ts +5 -0
- package/dist/runtime/svg-attributes.js +124 -0
- package/dist/runtime/svg-attributes.js.map +1 -0
- package/dist/runtime/to-vnode.d.ts +3 -0
- package/dist/runtime/to-vnode.js +43 -0
- package/dist/runtime/to-vnode.js.map +1 -0
- package/dist/runtime/typed-event-target.d.ts +50 -0
- package/dist/runtime/typed-event-target.js +6 -0
- package/dist/runtime/typed-event-target.js.map +1 -0
- package/dist/runtime/vdom.d.ts +55 -0
- package/dist/runtime/vdom.js +198 -0
- package/dist/runtime/vdom.js.map +1 -0
- package/dist/runtime/vnode.d.ts +79 -0
- package/dist/runtime/vnode.js +38 -0
- package/dist/runtime/vnode.js.map +1 -0
- package/dist/server/stream.d.ts +57 -0
- package/dist/server/stream.js +1007 -0
- package/dist/server/stream.js.map +1 -0
- package/dist/style/css-mixin.d.ts +8 -0
- package/dist/style/css-mixin.js +54 -0
- package/dist/style/css-mixin.js.map +1 -0
- package/dist/style/index.d.ts +5 -0
- package/dist/style/index.js +4 -0
- package/dist/style/index.js.map +1 -0
- package/dist/style/style.d.ts +24 -0
- package/dist/style/style.js +269 -0
- package/dist/style/style.js.map +1 -0
- package/dist/style/stylesheet.d.ts +11 -0
- package/dist/style/stylesheet.js +174 -0
- package/dist/style/stylesheet.js.map +1 -0
- package/dist/test.d.ts +2 -0
- package/dist/test.js +2 -0
- package/dist/test.js.map +1 -0
- package/dist/theme/contract.d.ts +259 -0
- package/dist/theme/contract.js +131 -0
- package/dist/theme/contract.js.map +1 -0
- package/dist/theme/glyph-contract.d.ts +13 -0
- package/dist/theme/glyph-contract.js +34 -0
- package/dist/theme/glyph-contract.js.map +1 -0
- package/dist/theme/presets/rmx-01/glyphs.d.ts +2 -0
- package/dist/theme/presets/rmx-01/glyphs.js +252 -0
- package/dist/theme/presets/rmx-01/glyphs.js.map +1 -0
- package/dist/theme/presets/rmx-01/index.d.ts +2 -0
- package/dist/theme/presets/rmx-01/index.js +119 -0
- package/dist/theme/presets/rmx-01/index.js.map +1 -0
- package/dist/theme/runtime.d.ts +2 -0
- package/dist/theme/runtime.js +81 -0
- package/dist/theme/runtime.js.map +1 -0
- package/dist/theme/theme.d.ts +6 -0
- package/dist/theme/theme.js +5 -0
- package/dist/theme/theme.js.map +1 -0
- package/dist/utils/flash-attribute.d.ts +1 -0
- package/dist/utils/flash-attribute.js +11 -0
- package/dist/utils/flash-attribute.js.map +1 -0
- package/dist/utils/scroll-lock.d.ts +3 -0
- package/dist/utils/scroll-lock.js +69 -0
- package/dist/utils/scroll-lock.js.map +1 -0
- package/dist/utils/wait-for-css-transition.d.ts +1 -0
- package/dist/utils/wait-for-css-transition.js +58 -0
- package/dist/utils/wait-for-css-transition.js.map +1 -0
- package/dist/utils/wait.d.ts +1 -0
- package/dist/utils/wait.js +6 -0
- package/dist/utils/wait.js.map +1 -0
- package/package.json +125 -5
- package/src/animation/animate-layout-mixin.ts +266 -0
- package/src/animation/animate-mixins.ts +292 -0
- package/src/animation/index.ts +6 -0
- package/src/animation/spring.ts +299 -0
- package/src/animation/tween.ts +134 -0
- package/src/components/accordion/accordion.tsx +565 -0
- package/src/components/anchor/anchor.ts +904 -0
- package/src/components/breadcrumbs/breadcrumbs.tsx +124 -0
- package/src/components/button/README.md +44 -0
- package/src/components/button/button.tsx +188 -0
- package/src/components/combobox/README.md +145 -0
- package/src/components/combobox/combobox.tsx +1014 -0
- package/src/components/glyph/glyph.tsx +110 -0
- package/src/components/listbox/listbox.ts +447 -0
- package/src/components/menu/hover-aim.ts +428 -0
- package/src/components/menu/menu.tsx +1547 -0
- package/src/components/popover/README.md +122 -0
- package/src/components/popover/popover.ts +201 -0
- package/src/components/select/select.tsx +585 -0
- package/src/components/separator/separator.ts +19 -0
- package/src/components/tabs/README.md +105 -0
- package/src/components/tabs/tabs.tsx +499 -0
- package/src/index.ts +72 -0
- package/src/interactions/keydown/keydown.ts +14 -0
- package/src/interactions/outside-click/outside-click-mixin.ts +55 -0
- package/src/interactions/typeahead/typeahead-mixin.ts +89 -0
- package/src/jsx-runtime.ts +2 -0
- package/src/runtime/client-entries.ts +137 -0
- package/src/runtime/component.ts +402 -0
- package/src/runtime/create-element.ts +38 -0
- package/src/runtime/diff-dom.ts +404 -0
- package/src/runtime/diff-props.ts +209 -0
- package/src/runtime/document-state.ts +130 -0
- package/src/runtime/dom.ts +3971 -0
- package/src/runtime/error-event.ts +26 -0
- package/src/runtime/event-listeners.ts +171 -0
- package/src/runtime/frame.ts +1211 -0
- package/src/runtime/invariant.ts +31 -0
- package/src/runtime/jsx.ts +398 -0
- package/src/runtime/mixins/attrs-mixin.ts +35 -0
- package/src/runtime/mixins/link-mixin.ts +131 -0
- package/src/runtime/mixins/mixin.ts +908 -0
- package/src/runtime/mixins/on-mixin.ts +89 -0
- package/src/runtime/mixins/ref-mixin.ts +32 -0
- package/src/runtime/navigation.ts +136 -0
- package/src/runtime/reconcile.ts +1722 -0
- package/src/runtime/render.ts +101 -0
- package/src/runtime/run.ts +100 -0
- package/src/runtime/scheduler.ts +262 -0
- package/src/runtime/svg-attributes.ts +133 -0
- package/src/runtime/to-vnode.ts +50 -0
- package/src/runtime/typed-event-target.ts +67 -0
- package/src/runtime/vdom.ts +295 -0
- package/src/runtime/vnode.ts +137 -0
- package/src/server/stream.ts +1342 -0
- package/src/style/css-mixin.ts +82 -0
- package/src/style/index.ts +10 -0
- package/src/style/style.ts +312 -0
- package/src/style/stylesheet.ts +209 -0
- package/src/test/setup.ts +7 -0
- package/src/test/utils.ts +45 -0
- package/src/test.ts +2 -0
- package/src/theme/contract.ts +183 -0
- package/src/theme/glyph-contract.ts +57 -0
- package/src/theme/presets/rmx-01/glyphs.tsx +308 -0
- package/src/theme/presets/rmx-01/index.ts +121 -0
- package/src/theme/runtime.ts +116 -0
- package/src/theme/theme.ts +15 -0
- package/src/utils/flash-attribute.ts +11 -0
- package/src/utils/scroll-lock.ts +97 -0
- package/src/utils/wait-for-css-transition.ts +66 -0
- package/src/utils/wait.ts +5 -0
|
@@ -0,0 +1,1211 @@
|
|
|
1
|
+
import { jsx } from './jsx.ts'
|
|
2
|
+
import { Frame, createFrameHandle, type FrameContent } from './component.ts'
|
|
3
|
+
import { createComponentErrorEvent, getComponentError } from './error-event.ts'
|
|
4
|
+
import { invariant } from './invariant.ts'
|
|
5
|
+
import type { RemixElement, RemixNode } from './jsx.ts'
|
|
6
|
+
import type { FrameHandle } from './component.ts'
|
|
7
|
+
import type { Scheduler, VirtualRoot } from './vdom.ts'
|
|
8
|
+
import { createRangeRoot, createRoot } from './vdom.ts'
|
|
9
|
+
import { diffNodes } from './diff-dom.ts'
|
|
10
|
+
import { createStyleManager, type StyleManager } from '../style/index.ts'
|
|
11
|
+
|
|
12
|
+
type FrameRoot = [Comment, Comment] | Element | Document | DocumentFragment
|
|
13
|
+
|
|
14
|
+
type FrameData = {
|
|
15
|
+
status: 'pending' | 'resolved'
|
|
16
|
+
name?: string
|
|
17
|
+
src: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type HydrationData = {
|
|
21
|
+
moduleUrl: string
|
|
22
|
+
exportName: string
|
|
23
|
+
props: Record<string, unknown>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type RmxData = {
|
|
27
|
+
h?: Record<string, HydrationData>
|
|
28
|
+
f?: Record<string, FrameData>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type VirtualRootMarker = Comment & {
|
|
32
|
+
$rmx: ReturnType<typeof createRangeRoot>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type FrameMarkerData = FrameData & {
|
|
36
|
+
id: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type PendingClientEntries = Map<Comment, [Comment, RemixElement]>
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Loads a client entry module for hydration.
|
|
43
|
+
*/
|
|
44
|
+
export type LoadModule = (moduleUrl: string, exportName: string) => Promise<Function> | Function
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolves frame content for the given frame source.
|
|
48
|
+
*/
|
|
49
|
+
export type ResolveFrame = (
|
|
50
|
+
src: string,
|
|
51
|
+
signal?: AbortSignal,
|
|
52
|
+
target?: string,
|
|
53
|
+
) => Promise<FrameContent> | FrameContent
|
|
54
|
+
|
|
55
|
+
type InternalFrameContent = FrameContent | DocumentFragment
|
|
56
|
+
|
|
57
|
+
type FrameTemplateListener = (fragment: DocumentFragment) => void
|
|
58
|
+
|
|
59
|
+
const bufferedFrameTemplates = new Map<string, DocumentFragment[]>()
|
|
60
|
+
const frameTemplateListeners = new Map<string, Set<FrameTemplateListener>>()
|
|
61
|
+
|
|
62
|
+
function syncElementAttributes(target: Element, source: Element) {
|
|
63
|
+
for (let attribute of Array.from(target.attributes)) {
|
|
64
|
+
if (!source.hasAttribute(attribute.name)) {
|
|
65
|
+
target.removeAttribute(attribute.name)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (let attribute of Array.from(source.attributes)) {
|
|
70
|
+
if (target.getAttribute(attribute.name) !== attribute.value) {
|
|
71
|
+
target.setAttribute(attribute.name, attribute.value)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type FrameRuntime = {
|
|
77
|
+
topFrame?: FrameHandle
|
|
78
|
+
errorTarget: EventTarget
|
|
79
|
+
loadModule: LoadModule
|
|
80
|
+
resolveFrame: ResolveFrame
|
|
81
|
+
pendingClientEntries: PendingClientEntries
|
|
82
|
+
scheduler: Scheduler
|
|
83
|
+
styleManager: StyleManager
|
|
84
|
+
data: RmxData
|
|
85
|
+
moduleCache: Map<string, Function>
|
|
86
|
+
moduleLoads: Map<string, Promise<Function | undefined>>
|
|
87
|
+
frameInstances: WeakMap<Comment, Frame>
|
|
88
|
+
namedFrames: Map<string, FrameHandle>
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type FrameContext = {
|
|
92
|
+
topFrame?: FrameHandle
|
|
93
|
+
errorTarget: EventTarget
|
|
94
|
+
loadModule: LoadModule
|
|
95
|
+
resolveFrame: ResolveFrame
|
|
96
|
+
pendingClientEntries: PendingClientEntries
|
|
97
|
+
scheduler: Scheduler
|
|
98
|
+
frame: FrameHandle
|
|
99
|
+
styleManager: StyleManager
|
|
100
|
+
data: RmxData
|
|
101
|
+
moduleCache: Map<string, Function>
|
|
102
|
+
moduleLoads: Map<string, Promise<Function | undefined>>
|
|
103
|
+
frameInstances: WeakMap<Comment, Frame>
|
|
104
|
+
namedFrames: Map<string, FrameHandle>
|
|
105
|
+
regionTailRef?: ChildNode | null
|
|
106
|
+
regionParent?: ParentNode | null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
type FrameInit = {
|
|
110
|
+
name?: string
|
|
111
|
+
topFrame?: FrameHandle
|
|
112
|
+
src: string
|
|
113
|
+
errorTarget: EventTarget
|
|
114
|
+
loadModule: LoadModule
|
|
115
|
+
resolveFrame: ResolveFrame
|
|
116
|
+
pendingClientEntries: PendingClientEntries
|
|
117
|
+
scheduler: Scheduler
|
|
118
|
+
styleManager?: StyleManager
|
|
119
|
+
marker?: FrameMarkerData
|
|
120
|
+
data: RmxData
|
|
121
|
+
moduleCache: Map<string, Function>
|
|
122
|
+
moduleLoads: Map<string, Promise<Function | undefined>>
|
|
123
|
+
frameInstances: WeakMap<Comment, Frame>
|
|
124
|
+
namedFrames: Map<string, FrameHandle>
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export type Frame = {
|
|
128
|
+
render: (content: InternalFrameContent, options?: RenderOptions) => Promise<void>
|
|
129
|
+
ready: () => Promise<void>
|
|
130
|
+
flush: () => void
|
|
131
|
+
dispose: () => void
|
|
132
|
+
handle: FrameHandle
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
type RenderOptions = {
|
|
136
|
+
initialHydrationTracker?: InitialHydrationTracker
|
|
137
|
+
signal?: AbortSignal
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function createFrame(root: FrameRoot, init: FrameInit): Frame {
|
|
141
|
+
let container = createContainer(root)
|
|
142
|
+
let observers: MutationObserver[] = []
|
|
143
|
+
let subscriptions: Array<() => void> = []
|
|
144
|
+
let contentRoot: VirtualRoot | undefined
|
|
145
|
+
let reloadController: AbortController | undefined
|
|
146
|
+
let styleManager = init.styleManager ?? createStyleManager()
|
|
147
|
+
|
|
148
|
+
// Merge any rmx-data found in the current document once at startup.
|
|
149
|
+
mergeRmxDataFromDocument(init.data, container.doc)
|
|
150
|
+
|
|
151
|
+
let runtime = createFrameRuntime({ ...init, styleManager })
|
|
152
|
+
|
|
153
|
+
let frame = createFrameHandle({
|
|
154
|
+
src: init.src,
|
|
155
|
+
$runtime: runtime,
|
|
156
|
+
reload: async () => {
|
|
157
|
+
reloadController?.abort()
|
|
158
|
+
let controller = new AbortController()
|
|
159
|
+
reloadController = controller
|
|
160
|
+
frame.dispatchEvent(new Event('reloadStart'))
|
|
161
|
+
try {
|
|
162
|
+
let content = await init.resolveFrame(frame.src, controller.signal, frameName)
|
|
163
|
+
if (reloadController !== controller || controller.signal.aborted) return controller.signal
|
|
164
|
+
await render(content, { signal: controller.signal })
|
|
165
|
+
return controller.signal
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (reloadController !== controller || controller.signal.aborted) return controller.signal
|
|
168
|
+
init.errorTarget.dispatchEvent(createComponentErrorEvent(error))
|
|
169
|
+
throw error
|
|
170
|
+
} finally {
|
|
171
|
+
if (reloadController === controller) {
|
|
172
|
+
frame.dispatchEvent(new Event('reloadComplete'))
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
replace: async (content: FrameContent) => {
|
|
177
|
+
await render(content)
|
|
178
|
+
},
|
|
179
|
+
})
|
|
180
|
+
runtime.topFrame = runtime.topFrame ?? init.topFrame ?? frame
|
|
181
|
+
|
|
182
|
+
let frameName = init.marker?.name ?? init.name
|
|
183
|
+
if (frameName) {
|
|
184
|
+
init.namedFrames.set(frameName, frame)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let context: FrameContext = {
|
|
188
|
+
topFrame: runtime.topFrame,
|
|
189
|
+
errorTarget: init.errorTarget,
|
|
190
|
+
loadModule: init.loadModule,
|
|
191
|
+
resolveFrame: init.resolveFrame,
|
|
192
|
+
pendingClientEntries: init.pendingClientEntries,
|
|
193
|
+
scheduler: init.scheduler,
|
|
194
|
+
frame,
|
|
195
|
+
styleManager,
|
|
196
|
+
data: init.data,
|
|
197
|
+
moduleCache: init.moduleCache,
|
|
198
|
+
moduleLoads: init.moduleLoads,
|
|
199
|
+
frameInstances: init.frameInstances,
|
|
200
|
+
namedFrames: init.namedFrames,
|
|
201
|
+
regionTailRef: container.regionTailRef,
|
|
202
|
+
regionParent: container.regionParent,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function render(content: InternalFrameContent, options?: RenderOptions): Promise<void> {
|
|
206
|
+
if (options?.signal?.aborted) return
|
|
207
|
+
|
|
208
|
+
if (content instanceof ReadableStream) {
|
|
209
|
+
await renderFrameStream(content, container.doc, async (html) => {
|
|
210
|
+
if (options?.signal?.aborted) return
|
|
211
|
+
await render(html, options)
|
|
212
|
+
})
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (isRemixNodeFrameContent(content)) {
|
|
217
|
+
if (!contentRoot) {
|
|
218
|
+
let currentNodes = getContentNodes()
|
|
219
|
+
removeVirtualRoots(currentNodes)
|
|
220
|
+
disposeSubFrames(currentNodes, context)
|
|
221
|
+
clearFrameContent()
|
|
222
|
+
contentRoot = createFrameContentRoot()
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (options?.signal?.aborted) return
|
|
226
|
+
contentRoot.render(content)
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (contentRoot) {
|
|
231
|
+
contentRoot.dispose()
|
|
232
|
+
contentRoot = undefined
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let isFullDocumentReload =
|
|
236
|
+
container.root instanceof Document &&
|
|
237
|
+
typeof content === 'string' &&
|
|
238
|
+
isFullDocumentHtml(content)
|
|
239
|
+
|
|
240
|
+
if (isFullDocumentReload && typeof content === 'string') {
|
|
241
|
+
// Full-document reload should tear down existing hydrated roots and subframes
|
|
242
|
+
// before diffing fresh HTML, otherwise stale component instances can survive
|
|
243
|
+
// on detached DOM nodes.
|
|
244
|
+
let previousBodyNodes = Array.from(container.doc.body.childNodes)
|
|
245
|
+
removeVirtualRoots(previousBodyNodes)
|
|
246
|
+
disposeSubFrames(previousBodyNodes, context)
|
|
247
|
+
let parsed = new DOMParser().parseFromString(content, 'text/html')
|
|
248
|
+
mergeRmxDataFromDocument(context.data, parsed)
|
|
249
|
+
context.styleManager.reset()
|
|
250
|
+
context.styleManager.adoptServerStyles(
|
|
251
|
+
collectFrameServerStyleTags(createElementContainer(parsed)),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
syncElementAttributes(container.doc.documentElement, parsed.documentElement)
|
|
255
|
+
syncElementAttributes(container.doc.head, parsed.head)
|
|
256
|
+
syncElementAttributes(container.doc.body, parsed.body)
|
|
257
|
+
|
|
258
|
+
diffNodes(Array.from(container.doc.head.childNodes), Array.from(parsed.head.childNodes), {
|
|
259
|
+
...context,
|
|
260
|
+
regionParent: container.doc.head,
|
|
261
|
+
regionTailRef: null,
|
|
262
|
+
})
|
|
263
|
+
diffNodes(Array.from(container.doc.body.childNodes), Array.from(parsed.body.childNodes), {
|
|
264
|
+
...context,
|
|
265
|
+
regionParent: container.doc.body,
|
|
266
|
+
regionTailRef: null,
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
let bodyContainer = createElementContainer(container.doc.body)
|
|
270
|
+
if (options?.signal?.aborted) return
|
|
271
|
+
scheduleHydrationInContainer(bodyContainer, context, options?.initialHydrationTracker)
|
|
272
|
+
createSubFrames(bodyContainer.childNodes, context)
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let fragment =
|
|
277
|
+
typeof content === 'string' ? createFragmentFromString(container.doc, content) : content
|
|
278
|
+
context.styleManager.reset()
|
|
279
|
+
context.styleManager.adoptServerStyles(
|
|
280
|
+
collectFrameServerStyleTags(createElementContainer(fragment)),
|
|
281
|
+
)
|
|
282
|
+
removeEmptyHeads(fragment)
|
|
283
|
+
mergeRmxDataFromFragment(context.data, fragment)
|
|
284
|
+
|
|
285
|
+
let nextContainer = createContainer(fragment)
|
|
286
|
+
|
|
287
|
+
if (options?.signal?.aborted) return
|
|
288
|
+
|
|
289
|
+
diffNodes(container.childNodes, Array.from(nextContainer.childNodes), {
|
|
290
|
+
...context,
|
|
291
|
+
regionTailRef: container.regionTailRef,
|
|
292
|
+
regionParent: container.regionParent,
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
if (options?.signal?.aborted) return
|
|
296
|
+
scheduleHydrationInContainer(container, context, options?.initialHydrationTracker)
|
|
297
|
+
createSubFrames(container.childNodes, context)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function createFrameContentRoot(): VirtualRoot {
|
|
301
|
+
let virtualRoot: VirtualRoot
|
|
302
|
+
if (container.root instanceof Document) {
|
|
303
|
+
virtualRoot = createRoot(container.doc.body, {
|
|
304
|
+
scheduler: context.scheduler,
|
|
305
|
+
frame,
|
|
306
|
+
styleManager: context.styleManager,
|
|
307
|
+
})
|
|
308
|
+
} else {
|
|
309
|
+
invariant(Array.isArray(root), 'Expected comment-bounded frame root')
|
|
310
|
+
virtualRoot = createRangeRoot(root, {
|
|
311
|
+
scheduler: context.scheduler,
|
|
312
|
+
frame,
|
|
313
|
+
styleManager: context.styleManager,
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
virtualRoot.addEventListener('error', (event: Event) => {
|
|
318
|
+
if (context.errorTarget === virtualRoot) return
|
|
319
|
+
context.errorTarget.dispatchEvent(createComponentErrorEvent(getComponentError(event)))
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
return virtualRoot
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function getContentNodes(): Node[] {
|
|
326
|
+
return container.root instanceof Document
|
|
327
|
+
? Array.from(container.doc.body.childNodes)
|
|
328
|
+
: container.childNodes
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function clearFrameContent() {
|
|
332
|
+
for (let node of getContentNodes()) {
|
|
333
|
+
node.parentNode?.removeChild(node)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function hydrateInitial(): Promise<void> {
|
|
338
|
+
let initialHydrationTracker = createInitialHydrationTracker()
|
|
339
|
+
|
|
340
|
+
context.styleManager.reset()
|
|
341
|
+
context.styleManager.adoptServerStyles(collectFrameServerStyleTags(container))
|
|
342
|
+
createSubFrames(container.childNodes, context)
|
|
343
|
+
scheduleHydrationInContainer(container, context, initialHydrationTracker)
|
|
344
|
+
|
|
345
|
+
if (init.marker?.status === 'pending') {
|
|
346
|
+
let markerId = init.marker.id
|
|
347
|
+
let early = consumeFrameTemplate(markerId) ?? getEarlyFrameContent(markerId)
|
|
348
|
+
if (early) {
|
|
349
|
+
await render(early, { initialHydrationTracker })
|
|
350
|
+
} else {
|
|
351
|
+
let observer = setupTemplateObserver()
|
|
352
|
+
let unsubscribe = subscribeFrameTemplate(markerId, async (fragment) => {
|
|
353
|
+
unsubscribe()
|
|
354
|
+
await render(fragment)
|
|
355
|
+
observer.disconnect()
|
|
356
|
+
})
|
|
357
|
+
subscriptions.push(unsubscribe)
|
|
358
|
+
let buffered = consumeFrameTemplate(markerId)
|
|
359
|
+
if (buffered) {
|
|
360
|
+
unsubscribe()
|
|
361
|
+
await render(buffered)
|
|
362
|
+
observer.disconnect()
|
|
363
|
+
}
|
|
364
|
+
observers.push(observer)
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
initialHydrationTracker.finalize()
|
|
369
|
+
await initialHydrationTracker.ready()
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function dispose(): void {
|
|
373
|
+
reloadController?.abort()
|
|
374
|
+
reloadController = undefined
|
|
375
|
+
contentRoot?.dispose()
|
|
376
|
+
contentRoot = undefined
|
|
377
|
+
|
|
378
|
+
// Disconnect any MutationObservers waiting for templates.
|
|
379
|
+
for (let observer of observers) {
|
|
380
|
+
observer.disconnect()
|
|
381
|
+
}
|
|
382
|
+
observers.length = 0
|
|
383
|
+
for (let unsubscribe of subscriptions) {
|
|
384
|
+
unsubscribe()
|
|
385
|
+
}
|
|
386
|
+
subscriptions.length = 0
|
|
387
|
+
|
|
388
|
+
// Remove hydrated virtual roots in this frame's region.
|
|
389
|
+
removeVirtualRoots(container.childNodes)
|
|
390
|
+
|
|
391
|
+
// Dispose sub-frames recursively.
|
|
392
|
+
disposeSubFrames(container.childNodes, context)
|
|
393
|
+
context.styleManager.dispose()
|
|
394
|
+
|
|
395
|
+
if (frameName) {
|
|
396
|
+
if (init.namedFrames.get(frameName) === frame) {
|
|
397
|
+
init.namedFrames.delete(frameName)
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
let readyPromise = hydrateInitial()
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
render,
|
|
406
|
+
ready: () => readyPromise,
|
|
407
|
+
flush: () => context.scheduler.dequeue(),
|
|
408
|
+
dispose,
|
|
409
|
+
handle: frame,
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export function createFrameRuntime(init: {
|
|
414
|
+
topFrame?: FrameHandle
|
|
415
|
+
errorTarget: EventTarget
|
|
416
|
+
loadModule: LoadModule
|
|
417
|
+
resolveFrame: ResolveFrame
|
|
418
|
+
pendingClientEntries: PendingClientEntries
|
|
419
|
+
scheduler: Scheduler
|
|
420
|
+
styleManager: StyleManager
|
|
421
|
+
data: RmxData
|
|
422
|
+
moduleCache: Map<string, Function>
|
|
423
|
+
moduleLoads: Map<string, Promise<Function | undefined>>
|
|
424
|
+
frameInstances: WeakMap<Comment, Frame>
|
|
425
|
+
namedFrames: Map<string, FrameHandle>
|
|
426
|
+
}): FrameRuntime {
|
|
427
|
+
return {
|
|
428
|
+
topFrame: init.topFrame,
|
|
429
|
+
errorTarget: init.errorTarget,
|
|
430
|
+
loadModule: init.loadModule,
|
|
431
|
+
resolveFrame: init.resolveFrame,
|
|
432
|
+
pendingClientEntries: init.pendingClientEntries,
|
|
433
|
+
scheduler: init.scheduler,
|
|
434
|
+
styleManager: init.styleManager,
|
|
435
|
+
data: init.data,
|
|
436
|
+
moduleCache: init.moduleCache,
|
|
437
|
+
moduleLoads: init.moduleLoads,
|
|
438
|
+
frameInstances: init.frameInstances,
|
|
439
|
+
namedFrames: init.namedFrames,
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
type InitialHydrationTracker = {
|
|
444
|
+
track: () => () => void
|
|
445
|
+
finalize: () => void
|
|
446
|
+
ready: () => Promise<void>
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function createInitialHydrationTracker(): InitialHydrationTracker {
|
|
450
|
+
let pending = 0
|
|
451
|
+
let finalized = false
|
|
452
|
+
|
|
453
|
+
let resolveReady: (() => void) | undefined
|
|
454
|
+
let readyPromise = new Promise<void>((resolve) => {
|
|
455
|
+
resolveReady = resolve
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
function maybeResolve() {
|
|
459
|
+
if (finalized && pending === 0) {
|
|
460
|
+
resolveReady?.()
|
|
461
|
+
resolveReady = undefined
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
track() {
|
|
467
|
+
pending++
|
|
468
|
+
let completed = false
|
|
469
|
+
return () => {
|
|
470
|
+
if (completed) return
|
|
471
|
+
completed = true
|
|
472
|
+
pending--
|
|
473
|
+
maybeResolve()
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
finalize() {
|
|
477
|
+
finalized = true
|
|
478
|
+
maybeResolve()
|
|
479
|
+
},
|
|
480
|
+
ready() {
|
|
481
|
+
return readyPromise
|
|
482
|
+
},
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function mergeRmxDataFromDocument(into: RmxData, doc: Document): void {
|
|
487
|
+
let scripts = Array.from(doc.querySelectorAll('script#rmx-data'))
|
|
488
|
+
for (let script of scripts) {
|
|
489
|
+
if (!(script instanceof HTMLScriptElement)) continue
|
|
490
|
+
mergeRmxData(into, parseRmxDataScript(script))
|
|
491
|
+
script.remove()
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function mergeRmxDataFromFragment(into: RmxData, fragment: DocumentFragment): void {
|
|
496
|
+
let scripts = Array.from(fragment.querySelectorAll('script#rmx-data'))
|
|
497
|
+
for (let script of scripts) {
|
|
498
|
+
if (!(script instanceof HTMLScriptElement)) continue
|
|
499
|
+
mergeRmxData(into, parseRmxDataScript(script))
|
|
500
|
+
script.remove()
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function removeEmptyHeads(fragment: DocumentFragment): void {
|
|
505
|
+
let heads = Array.from(fragment.querySelectorAll('head'))
|
|
506
|
+
for (let head of heads) {
|
|
507
|
+
if (!head.childNodes.length) {
|
|
508
|
+
head.remove()
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function collectFrameServerStyleTags(container: FrameContainer): HTMLStyleElement[] {
|
|
514
|
+
let styles: HTMLStyleElement[] = []
|
|
515
|
+
let nodes =
|
|
516
|
+
container.root instanceof Document
|
|
517
|
+
? [...Array.from(container.doc.head.childNodes), ...Array.from(container.doc.body.childNodes)]
|
|
518
|
+
: container.childNodes
|
|
519
|
+
|
|
520
|
+
collectOwnedServerStyleTags(nodes, styles)
|
|
521
|
+
return styles
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function collectOwnedServerStyleTags(nodes: Node[], styles: HTMLStyleElement[]): void {
|
|
525
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
526
|
+
let node = nodes[i]
|
|
527
|
+
|
|
528
|
+
if (isFrameStart(node)) {
|
|
529
|
+
let end = findEndMarker(node, isFrameStart, isFrameEnd)
|
|
530
|
+
i = nodes.indexOf(end)
|
|
531
|
+
continue
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (node instanceof HTMLStyleElement && node.matches('style[data-rmx]')) {
|
|
535
|
+
styles.push(node)
|
|
536
|
+
continue
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (node instanceof Element || node instanceof Document || node instanceof DocumentFragment) {
|
|
540
|
+
collectOwnedServerStyleTags(Array.from(node.childNodes), styles)
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function parseRmxDataScript(script: HTMLScriptElement): RmxData {
|
|
546
|
+
try {
|
|
547
|
+
return JSON.parse(script.textContent || '{}')
|
|
548
|
+
} catch {
|
|
549
|
+
console.error('[createFrame] Failed to parse rmx-data script')
|
|
550
|
+
return {}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function mergeRmxData(into: RmxData, from: RmxData): void {
|
|
555
|
+
if (from.h) {
|
|
556
|
+
if (!into.h) into.h = {}
|
|
557
|
+
copyOwnRmxEntries(into.h, from.h)
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (from.f) {
|
|
561
|
+
if (!into.f) into.f = {}
|
|
562
|
+
copyOwnRmxEntries(into.f, from.f)
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function copyOwnRmxEntries<T>(target: Record<string, T>, source: Record<string, T>): void {
|
|
567
|
+
for (let key of Object.keys(source)) {
|
|
568
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue
|
|
569
|
+
if (!Object.hasOwn(source, key)) continue
|
|
570
|
+
target[key] = source[key]!
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function scheduleHydrationInContainer(
|
|
575
|
+
container: FrameContainer,
|
|
576
|
+
context: FrameContext,
|
|
577
|
+
initialHydrationTracker?: InitialHydrationTracker,
|
|
578
|
+
): void {
|
|
579
|
+
let hydrationMarkers = findHydrationMarkers(container)
|
|
580
|
+
if (hydrationMarkers.length === 0) return
|
|
581
|
+
|
|
582
|
+
let hydrationData = context.data.h
|
|
583
|
+
if (!hydrationData) return
|
|
584
|
+
|
|
585
|
+
for (let marker of hydrationMarkers) {
|
|
586
|
+
let entry = hydrationData[marker.id]
|
|
587
|
+
if (!entry) continue
|
|
588
|
+
scheduleHydrationMarker(marker, entry, context, initialHydrationTracker)
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function scheduleHydrationMarker(
|
|
593
|
+
marker: HydrationMarker,
|
|
594
|
+
entry: HydrationData,
|
|
595
|
+
context: FrameContext,
|
|
596
|
+
initialHydrationTracker?: InitialHydrationTracker,
|
|
597
|
+
): void {
|
|
598
|
+
let done = initialHydrationTracker?.track()
|
|
599
|
+
let key = `${entry.moduleUrl}#${entry.exportName}`
|
|
600
|
+
|
|
601
|
+
let hydrateWithComponent = (component: Function) => {
|
|
602
|
+
if (!isHydrationMarkerLive(marker, context)) return
|
|
603
|
+
let vElement = createElement(component, entry.props)
|
|
604
|
+
context.pendingClientEntries.set(marker.start, [marker.end, vElement])
|
|
605
|
+
hydrateRegion(vElement, marker.start, marker.end, context)
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
let cached = context.moduleCache.get(key)
|
|
609
|
+
if (cached) {
|
|
610
|
+
hydrateWithComponent(cached)
|
|
611
|
+
done?.()
|
|
612
|
+
return
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
getOrStartModuleLoad(key, entry, marker.id, context)
|
|
616
|
+
.then((component) => {
|
|
617
|
+
if (component) {
|
|
618
|
+
hydrateWithComponent(component)
|
|
619
|
+
}
|
|
620
|
+
})
|
|
621
|
+
.finally(() => {
|
|
622
|
+
done?.()
|
|
623
|
+
})
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function getOrStartModuleLoad(
|
|
627
|
+
key: string,
|
|
628
|
+
entry: HydrationData,
|
|
629
|
+
markerId: string,
|
|
630
|
+
context: FrameContext,
|
|
631
|
+
): Promise<Function | undefined> {
|
|
632
|
+
let inFlight = context.moduleLoads.get(key)
|
|
633
|
+
if (inFlight) return inFlight
|
|
634
|
+
|
|
635
|
+
let loadPromise = (async () => {
|
|
636
|
+
try {
|
|
637
|
+
let mod = await context.loadModule(entry.moduleUrl, entry.exportName)
|
|
638
|
+
if (typeof mod !== 'function') {
|
|
639
|
+
throw new Error(`Export "${entry.exportName}" from "${entry.moduleUrl}" is not a function`)
|
|
640
|
+
}
|
|
641
|
+
context.moduleCache.set(key, mod)
|
|
642
|
+
return mod
|
|
643
|
+
} catch (error) {
|
|
644
|
+
console.error(`[createFrame] Failed to load module for ${markerId}:`, error)
|
|
645
|
+
return undefined
|
|
646
|
+
} finally {
|
|
647
|
+
context.moduleLoads.delete(key)
|
|
648
|
+
}
|
|
649
|
+
})()
|
|
650
|
+
|
|
651
|
+
context.moduleLoads.set(key, loadPromise)
|
|
652
|
+
return loadPromise
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function createElement(component: Function, props: Record<string, unknown>): RemixElement {
|
|
656
|
+
let revivedProps = reviveSerializedValue(props)
|
|
657
|
+
return jsx(component, revivedProps as any)
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function reviveSerializedValue(value: unknown): unknown {
|
|
661
|
+
if (value === null || value === undefined) return value
|
|
662
|
+
if (typeof value !== 'object') return value
|
|
663
|
+
|
|
664
|
+
if (Array.isArray(value)) {
|
|
665
|
+
return value.map((item) => reviveSerializedValue(item))
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
let record = value as Record<string, unknown>
|
|
669
|
+
|
|
670
|
+
if (record.$rmxFrame === true) {
|
|
671
|
+
let props = reviveSerializedObject(record.props)
|
|
672
|
+
let key = reviveSerializedValue(record.key)
|
|
673
|
+
return jsx(Frame as any, props as any, key as any)
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (record.$rmx === true && typeof record.type === 'string') {
|
|
677
|
+
let props = reviveSerializedObject(record.props)
|
|
678
|
+
let key = reviveSerializedValue(record.key)
|
|
679
|
+
return jsx(record.type as any, props as any, key as any)
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
let revived: Record<string, unknown> = {}
|
|
683
|
+
for (let key in record) {
|
|
684
|
+
revived[key] = reviveSerializedValue(record[key])
|
|
685
|
+
}
|
|
686
|
+
return revived
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function reviveSerializedObject(value: unknown): Record<string, unknown> {
|
|
690
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return {}
|
|
691
|
+
let revived = reviveSerializedValue(value)
|
|
692
|
+
if (!revived || typeof revived !== 'object' || Array.isArray(revived)) return {}
|
|
693
|
+
return revived as Record<string, unknown>
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function hydrateRegion(
|
|
697
|
+
vElement: RemixElement,
|
|
698
|
+
start: Comment,
|
|
699
|
+
end: Comment,
|
|
700
|
+
context: FrameContext,
|
|
701
|
+
): void {
|
|
702
|
+
context.pendingClientEntries.delete(start)
|
|
703
|
+
|
|
704
|
+
// The same marker can be discovered by overlapping hydration passes
|
|
705
|
+
// (for example, document root + nested frame root). Reuse the existing
|
|
706
|
+
// virtual root instead of redefining the marker property.
|
|
707
|
+
if (isHydratedVirtualRootMarker(start)) {
|
|
708
|
+
start.$rmx.render(vElement)
|
|
709
|
+
return
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
let root = createRangeRoot([start, end], {
|
|
713
|
+
scheduler: context.scheduler,
|
|
714
|
+
frame: context.frame,
|
|
715
|
+
styleManager: context.styleManager,
|
|
716
|
+
})
|
|
717
|
+
root.addEventListener('error', (event) => {
|
|
718
|
+
if (context.errorTarget === root) return
|
|
719
|
+
context.errorTarget.dispatchEvent(createComponentErrorEvent(getComponentError(event)))
|
|
720
|
+
})
|
|
721
|
+
|
|
722
|
+
Object.defineProperty(start, '$rmx', { value: root, enumerable: false })
|
|
723
|
+
root.render(vElement)
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function createSubFrames(nodes: Node[], context: FrameContext) {
|
|
727
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
728
|
+
let node = nodes[i]
|
|
729
|
+
|
|
730
|
+
if (isFrameStart(node)) {
|
|
731
|
+
let end = findEndMarker(node, isFrameStart, isFrameEnd)
|
|
732
|
+
|
|
733
|
+
if (!context.frameInstances.has(node)) {
|
|
734
|
+
let id = getFrameId(node)
|
|
735
|
+
let marker = context.data.f?.[id]
|
|
736
|
+
if (marker) {
|
|
737
|
+
let frameMarker: FrameMarkerData = { ...marker, id }
|
|
738
|
+
let subFrame = createFrame([node, end], {
|
|
739
|
+
src: frameMarker.src,
|
|
740
|
+
marker: frameMarker,
|
|
741
|
+
topFrame: context.topFrame,
|
|
742
|
+
errorTarget: context.errorTarget,
|
|
743
|
+
loadModule: context.loadModule,
|
|
744
|
+
resolveFrame: context.resolveFrame,
|
|
745
|
+
pendingClientEntries: context.pendingClientEntries,
|
|
746
|
+
scheduler: context.scheduler,
|
|
747
|
+
data: context.data,
|
|
748
|
+
moduleCache: context.moduleCache,
|
|
749
|
+
moduleLoads: context.moduleLoads,
|
|
750
|
+
frameInstances: context.frameInstances,
|
|
751
|
+
namedFrames: context.namedFrames,
|
|
752
|
+
})
|
|
753
|
+
context.frameInstances.set(node, subFrame)
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
i = nodes.indexOf(end)
|
|
758
|
+
continue
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (node.childNodes && node.childNodes.length > 0) {
|
|
762
|
+
createSubFrames(Array.from(node.childNodes), context)
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function isHydrationMarkerLive(marker: HydrationMarker, context: FrameContext): boolean {
|
|
768
|
+
if (!marker.start.isConnected || !marker.end.isConnected) return false
|
|
769
|
+
if (marker.start.parentNode !== marker.end.parentNode) return false
|
|
770
|
+
|
|
771
|
+
let startText = marker.start.data.trim()
|
|
772
|
+
if (startText !== `rmx:h:${marker.id}`) return false
|
|
773
|
+
if (marker.end.data.trim() !== '/rmx:h') return false
|
|
774
|
+
|
|
775
|
+
let parent = marker.start.parentNode
|
|
776
|
+
if (!parent) return false
|
|
777
|
+
|
|
778
|
+
if (context.regionTailRef) {
|
|
779
|
+
let startPosition = marker.start.compareDocumentPosition(context.regionTailRef)
|
|
780
|
+
let endPosition = marker.end.compareDocumentPosition(context.regionTailRef)
|
|
781
|
+
let tailFollowsStart = (startPosition & Node.DOCUMENT_POSITION_FOLLOWING) !== 0
|
|
782
|
+
let tailFollowsEnd = (endPosition & Node.DOCUMENT_POSITION_FOLLOWING) !== 0
|
|
783
|
+
if (!tailFollowsStart || !tailFollowsEnd) return false
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
return true
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function removeVirtualRoots(nodes: Node[]): void {
|
|
790
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
791
|
+
let node = nodes[i]
|
|
792
|
+
|
|
793
|
+
if (isHydratedVirtualRootMarker(node)) {
|
|
794
|
+
node.$rmx.dispose()
|
|
795
|
+
let end = findEndMarker(node, isHydrationStart, isHydrationEnd)
|
|
796
|
+
i = nodes.indexOf(end)
|
|
797
|
+
continue
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (node.childNodes && node.childNodes.length > 0) {
|
|
801
|
+
removeVirtualRoots(Array.from(node.childNodes))
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function disposeSubFrames(nodes: Node[], context: FrameContext): void {
|
|
807
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
808
|
+
let node = nodes[i]
|
|
809
|
+
|
|
810
|
+
if (isFrameStart(node)) {
|
|
811
|
+
let end = findEndMarker(node, isFrameStart, isFrameEnd)
|
|
812
|
+
let subFrame = context.frameInstances.get(node)
|
|
813
|
+
if (subFrame) {
|
|
814
|
+
subFrame.dispose()
|
|
815
|
+
context.frameInstances.delete(node)
|
|
816
|
+
}
|
|
817
|
+
i = nodes.indexOf(end)
|
|
818
|
+
continue
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if (node.childNodes && node.childNodes.length > 0) {
|
|
822
|
+
disposeSubFrames(Array.from(node.childNodes), context)
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function getEarlyFrameContent(id: string): DocumentFragment | null {
|
|
828
|
+
let template = document.querySelector(`template#${id}`)
|
|
829
|
+
if (template instanceof HTMLTemplateElement) {
|
|
830
|
+
let fragment = template.content
|
|
831
|
+
template.remove()
|
|
832
|
+
return fragment
|
|
833
|
+
}
|
|
834
|
+
return null
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function setupTemplateObserver(): MutationObserver {
|
|
838
|
+
let root = document.body ?? document.documentElement ?? document
|
|
839
|
+
let observer = new MutationObserver((mutations) => {
|
|
840
|
+
for (let mutation of mutations) {
|
|
841
|
+
for (let node of mutation.addedNodes) {
|
|
842
|
+
collectAndPublishTemplates(node)
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
})
|
|
846
|
+
|
|
847
|
+
observer.observe(root, { childList: true, subtree: true })
|
|
848
|
+
return observer
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function collectAndPublishTemplates(node: Node): void {
|
|
852
|
+
if (node instanceof HTMLTemplateElement) {
|
|
853
|
+
publishFrameTemplateElement(node)
|
|
854
|
+
return
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
if (!(node instanceof Element)) return
|
|
858
|
+
let templates = Array.from(node.querySelectorAll('template'))
|
|
859
|
+
for (let template of templates) {
|
|
860
|
+
if (!(template instanceof HTMLTemplateElement)) continue
|
|
861
|
+
publishFrameTemplateElement(template)
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function publishFrameTemplateElement(template: HTMLTemplateElement): void {
|
|
866
|
+
if (!template.id) return
|
|
867
|
+
template.remove()
|
|
868
|
+
publishFrameTemplate(template.id, template.content)
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
export function publishFrameTemplate(id: string, fragment: DocumentFragment): void {
|
|
872
|
+
let listeners = frameTemplateListeners.get(id)
|
|
873
|
+
if (!listeners || listeners.size === 0) {
|
|
874
|
+
let queue = bufferedFrameTemplates.get(id)
|
|
875
|
+
if (!queue) {
|
|
876
|
+
queue = []
|
|
877
|
+
bufferedFrameTemplates.set(id, queue)
|
|
878
|
+
}
|
|
879
|
+
queue.push(fragment)
|
|
880
|
+
return
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
for (let listener of listeners) {
|
|
884
|
+
listener(fragment.cloneNode(true) as DocumentFragment)
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
export function consumeFrameTemplate(id: string): DocumentFragment | null {
|
|
889
|
+
let queue = bufferedFrameTemplates.get(id)
|
|
890
|
+
if (!queue || queue.length === 0) return null
|
|
891
|
+
|
|
892
|
+
let fragment = queue.shift() ?? null
|
|
893
|
+
if (queue.length === 0) {
|
|
894
|
+
bufferedFrameTemplates.delete(id)
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
return fragment
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function subscribeFrameTemplate(id: string, listener: FrameTemplateListener): () => void {
|
|
901
|
+
let listeners = frameTemplateListeners.get(id)
|
|
902
|
+
if (!listeners) {
|
|
903
|
+
listeners = new Set()
|
|
904
|
+
frameTemplateListeners.set(id, listeners)
|
|
905
|
+
}
|
|
906
|
+
listeners.add(listener)
|
|
907
|
+
return () => {
|
|
908
|
+
let current = frameTemplateListeners.get(id)
|
|
909
|
+
if (!current) return
|
|
910
|
+
current.delete(listener)
|
|
911
|
+
if (current.size === 0) {
|
|
912
|
+
frameTemplateListeners.delete(id)
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
type StreamTemplateParseResult = {
|
|
918
|
+
html: string
|
|
919
|
+
remainder: string
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
const COMPLETE_TEMPLATE_WITH_ID_PATTERN =
|
|
923
|
+
/<template\b[^>]*\bid=(?:"([^"]+)"|'([^']+)')[^>]*>[\s\S]*?<\/template>/gi
|
|
924
|
+
|
|
925
|
+
function extractTemplatesFromBuffer(
|
|
926
|
+
doc: Document,
|
|
927
|
+
buffer: string,
|
|
928
|
+
onTemplate: (id: string, fragment: DocumentFragment) => void,
|
|
929
|
+
): StreamTemplateParseResult {
|
|
930
|
+
let html = ''
|
|
931
|
+
let cursor = 0
|
|
932
|
+
let hadMatch = false
|
|
933
|
+
|
|
934
|
+
COMPLETE_TEMPLATE_WITH_ID_PATTERN.lastIndex = 0
|
|
935
|
+
let match = COMPLETE_TEMPLATE_WITH_ID_PATTERN.exec(buffer)
|
|
936
|
+
|
|
937
|
+
while (match) {
|
|
938
|
+
hadMatch = true
|
|
939
|
+
let index = match.index
|
|
940
|
+
let fullMatch = match[0]
|
|
941
|
+
let id = match[1] ?? match[2]
|
|
942
|
+
let matchEnd = index + fullMatch.length
|
|
943
|
+
|
|
944
|
+
html += buffer.slice(cursor, index)
|
|
945
|
+
|
|
946
|
+
if (id) {
|
|
947
|
+
let parsed = createFragmentFromString(doc, fullMatch)
|
|
948
|
+
let template = parsed.querySelector('template')
|
|
949
|
+
if (template instanceof HTMLTemplateElement && template.id) {
|
|
950
|
+
onTemplate(template.id, template.content)
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
cursor = matchEnd
|
|
955
|
+
match = COMPLETE_TEMPLATE_WITH_ID_PATTERN.exec(buffer)
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
let tail = buffer.slice(cursor)
|
|
959
|
+
if (tail === '') return { html, remainder: '' }
|
|
960
|
+
|
|
961
|
+
let tailStart = tail.toLowerCase().lastIndexOf('<template')
|
|
962
|
+
if (tailStart === -1) {
|
|
963
|
+
return { html: html + tail, remainder: '' }
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (!hadMatch) {
|
|
967
|
+
return {
|
|
968
|
+
html: buffer.slice(0, tailStart),
|
|
969
|
+
remainder: buffer.slice(tailStart),
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
return {
|
|
974
|
+
html: html + tail.slice(0, tailStart),
|
|
975
|
+
remainder: tail.slice(tailStart),
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
async function renderFrameStream(
|
|
980
|
+
stream: ReadableStream<Uint8Array>,
|
|
981
|
+
doc: Document,
|
|
982
|
+
applyHtml: (html: string) => Promise<void>,
|
|
983
|
+
): Promise<void> {
|
|
984
|
+
let reader = stream.getReader()
|
|
985
|
+
let decoder = new TextDecoder()
|
|
986
|
+
let buffer = ''
|
|
987
|
+
let html = ''
|
|
988
|
+
let appliedLength = 0
|
|
989
|
+
let appliedOnce = false
|
|
990
|
+
|
|
991
|
+
try {
|
|
992
|
+
while (true) {
|
|
993
|
+
let { done, value } = await reader.read()
|
|
994
|
+
if (done) break
|
|
995
|
+
|
|
996
|
+
buffer += decoder.decode(value, { stream: true })
|
|
997
|
+
let parsed = extractTemplatesFromBuffer(doc, buffer, publishFrameTemplate)
|
|
998
|
+
buffer = parsed.remainder
|
|
999
|
+
|
|
1000
|
+
if (parsed.html !== '') {
|
|
1001
|
+
html += parsed.html
|
|
1002
|
+
let htmlMarkers = collectHtmlMarkerSummary(html)
|
|
1003
|
+
if (!hasBalancedMarkerSummary(htmlMarkers)) {
|
|
1004
|
+
continue
|
|
1005
|
+
}
|
|
1006
|
+
await applyHtml(html)
|
|
1007
|
+
appliedLength = html.length
|
|
1008
|
+
appliedOnce = true
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
buffer += decoder.decode()
|
|
1013
|
+
let parsed = extractTemplatesFromBuffer(doc, buffer, publishFrameTemplate)
|
|
1014
|
+
html += parsed.html
|
|
1015
|
+
buffer = parsed.remainder
|
|
1016
|
+
if (buffer !== '') {
|
|
1017
|
+
html += buffer
|
|
1018
|
+
buffer = ''
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
if (html !== '' && html.length > appliedLength) {
|
|
1022
|
+
await applyHtml(html)
|
|
1023
|
+
appliedOnce = true
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// A frame stream can legitimately resolve to empty content. Ensure the
|
|
1027
|
+
// existing frame region is cleared instead of treated as a no-op.
|
|
1028
|
+
if (html === '' && !appliedOnce) {
|
|
1029
|
+
await applyHtml('')
|
|
1030
|
+
}
|
|
1031
|
+
} finally {
|
|
1032
|
+
reader.releaseLock()
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
type FrameContainer = {
|
|
1037
|
+
doc: Document
|
|
1038
|
+
root: ParentNode
|
|
1039
|
+
childNodes: Node[]
|
|
1040
|
+
regionTailRef?: ChildNode | null
|
|
1041
|
+
regionParent?: ParentNode | null
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function createContainer(root: FrameRoot): FrameContainer {
|
|
1045
|
+
return Array.isArray(root) ? createCommentContainer(root) : createElementContainer(root)
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function createElementContainer(root: Document | Element | DocumentFragment): FrameContainer {
|
|
1049
|
+
let doc = root instanceof Document ? root : (root.ownerDocument ?? document)
|
|
1050
|
+
return {
|
|
1051
|
+
doc,
|
|
1052
|
+
root,
|
|
1053
|
+
get childNodes() {
|
|
1054
|
+
return Array.from(root.childNodes)
|
|
1055
|
+
},
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function createCommentContainer([start, end]: [Comment, Comment]): FrameContainer {
|
|
1060
|
+
let parent = end.parentNode
|
|
1061
|
+
invariant(parent, 'Invalid comment container')
|
|
1062
|
+
invariant(start.parentNode === parent, 'Boundaries must share parent')
|
|
1063
|
+
let doc = parent.ownerDocument ?? document
|
|
1064
|
+
|
|
1065
|
+
let getChildNodesBetween = (): Node[] => {
|
|
1066
|
+
let nodes: Node[] = []
|
|
1067
|
+
let node = start.nextSibling
|
|
1068
|
+
while (node && node !== end) {
|
|
1069
|
+
nodes.push(node)
|
|
1070
|
+
node = node.nextSibling
|
|
1071
|
+
}
|
|
1072
|
+
return nodes
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
return {
|
|
1076
|
+
doc,
|
|
1077
|
+
root: parent,
|
|
1078
|
+
get childNodes() {
|
|
1079
|
+
return getChildNodesBetween()
|
|
1080
|
+
},
|
|
1081
|
+
regionTailRef: end,
|
|
1082
|
+
regionParent: parent,
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function createFragmentFromString(doc: Document, content: string): DocumentFragment {
|
|
1087
|
+
let template = doc.createElement('template')
|
|
1088
|
+
template.innerHTML = content.trim()
|
|
1089
|
+
return template.content
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function isRemixNodeFrameContent(content: InternalFrameContent): content is RemixNode {
|
|
1093
|
+
return !(
|
|
1094
|
+
content instanceof ReadableStream ||
|
|
1095
|
+
content instanceof DocumentFragment ||
|
|
1096
|
+
typeof content === 'string'
|
|
1097
|
+
)
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function isFullDocumentHtml(content: string): boolean {
|
|
1101
|
+
let trimmed = content.trimStart()
|
|
1102
|
+
return /^<!doctype html\b/i.test(trimmed) || /^<html[\s>]/i.test(trimmed)
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
type HydrationMarker = {
|
|
1106
|
+
id: string
|
|
1107
|
+
start: Comment
|
|
1108
|
+
end: Comment
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
function findHydrationMarkers(container: FrameContainer): HydrationMarker[] {
|
|
1112
|
+
let results: HydrationMarker[] = []
|
|
1113
|
+
|
|
1114
|
+
forEachComment(container, (comment) => {
|
|
1115
|
+
let trimmed = comment.data.trim()
|
|
1116
|
+
if (!trimmed.startsWith('rmx:h:')) return
|
|
1117
|
+
|
|
1118
|
+
let id = trimmed.slice('rmx:h:'.length)
|
|
1119
|
+
let end = findEndMarker(comment, isHydrationStart, isHydrationEnd)
|
|
1120
|
+
results.push({ id, start: comment, end })
|
|
1121
|
+
})
|
|
1122
|
+
|
|
1123
|
+
return results
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function forEachComment(container: FrameContainer, cb: (comment: Comment) => void): void {
|
|
1127
|
+
walkCommentsInNodes(container.childNodes, cb)
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function walkCommentsInNodes(nodes: Node[], cb: (comment: Comment) => void): void {
|
|
1131
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
1132
|
+
let node = nodes[i]
|
|
1133
|
+
|
|
1134
|
+
// Frame ownership boundary: hydration markers inside nested frame regions
|
|
1135
|
+
// are discovered and hydrated by the nested frame instance only.
|
|
1136
|
+
if (isFrameStart(node)) {
|
|
1137
|
+
let end = findEndMarker(node, isFrameStart, isFrameEnd)
|
|
1138
|
+
i = nodes.indexOf(end)
|
|
1139
|
+
continue
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
if (node.nodeType === Node.COMMENT_NODE) cb(node as Comment)
|
|
1143
|
+
if (node.childNodes && node.childNodes.length > 0) {
|
|
1144
|
+
walkCommentsInNodes(Array.from(node.childNodes), cb)
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function isHydrationStart(node: Comment): boolean {
|
|
1150
|
+
return node.data.trim().startsWith('rmx:h:')
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
function isHydrationEnd(node: Comment): boolean {
|
|
1154
|
+
return node.data.trim() === '/rmx:h'
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
function isHydratedVirtualRootMarker(node: Node): node is VirtualRootMarker {
|
|
1158
|
+
return node instanceof Comment && '$rmx' in node
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function isFrameStart(node: Node): node is Comment {
|
|
1162
|
+
return node instanceof Comment && node.data.trim().startsWith('rmx:f:')
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
function isFrameEnd(node: Comment): boolean {
|
|
1166
|
+
return node.data.trim() === '/rmx:f'
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function getFrameId(start: Comment): string {
|
|
1170
|
+
let trimmed = start.data.trim()
|
|
1171
|
+
invariant(trimmed.startsWith('rmx:f:'), 'Invalid frame start marker')
|
|
1172
|
+
return trimmed.slice('rmx:f:'.length)
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function findEndMarker(
|
|
1176
|
+
start: Comment,
|
|
1177
|
+
isStart: (node: Comment) => boolean,
|
|
1178
|
+
isEnd: (node: Comment) => boolean,
|
|
1179
|
+
): Comment {
|
|
1180
|
+
let node: Node | null = start.nextSibling
|
|
1181
|
+
let depth = 1
|
|
1182
|
+
|
|
1183
|
+
while (node) {
|
|
1184
|
+
if (node.nodeType === Node.COMMENT_NODE) {
|
|
1185
|
+
let comment = node as Comment
|
|
1186
|
+
if (isStart(comment)) depth++
|
|
1187
|
+
else if (isEnd(comment)) {
|
|
1188
|
+
depth--
|
|
1189
|
+
if (depth === 0) return comment
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
node = node.nextSibling
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
throw new Error('End marker not found')
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function collectHtmlMarkerSummary(html: string): Record<string, number> {
|
|
1199
|
+
return {
|
|
1200
|
+
frameStarts: html.match(/<!--\s*rmx:f:/g)?.length ?? 0,
|
|
1201
|
+
frameEnds: html.match(/<!--\s*\/rmx:f\s*-->/g)?.length ?? 0,
|
|
1202
|
+
hydrationStarts: html.match(/<!--\s*rmx:h:/g)?.length ?? 0,
|
|
1203
|
+
hydrationEnds: html.match(/<!--\s*\/rmx:h\s*-->/g)?.length ?? 0,
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function hasBalancedMarkerSummary(summary: Record<string, number>): boolean {
|
|
1208
|
+
return (
|
|
1209
|
+
summary.frameStarts === summary.frameEnds && summary.hydrationStarts === summary.hydrationEnds
|
|
1210
|
+
)
|
|
1211
|
+
}
|