@remix-run/ui 0.4.0 → 0.6.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.
Files changed (195) hide show
  1. package/README.md +74 -1
  2. package/dist/accordion/index.js +2 -2
  3. package/dist/accordion/primitives.d.ts +1 -1
  4. package/dist/accordion/primitives.js.map +1 -1
  5. package/dist/anchor/index.js.map +1 -1
  6. package/dist/animation/animate-layout-mixin.js +1 -1
  7. package/dist/animation/animate-layout-mixin.js.map +1 -1
  8. package/dist/animation/animate-mixins.js +2 -2
  9. package/dist/animation/animate-mixins.js.map +1 -1
  10. package/dist/animation/demos/color-interpolation.js +1 -1
  11. package/dist/animation/demos/color-interpolation.js.map +1 -1
  12. package/dist/animation/demos/drag-release.d.ts +1 -1
  13. package/dist/animation/demos/hold-to-confirm.d.ts +3 -3
  14. package/dist/animation/demos/material-ripple.js +1 -3
  15. package/dist/animation/demos/material-ripple.js.map +1 -1
  16. package/dist/animation/index.js +4 -4
  17. package/dist/animation/spring.js.map +1 -1
  18. package/dist/breadcrumbs/index.js +2 -2
  19. package/dist/button/index.js +25 -25
  20. package/dist/button/index.js.map +1 -1
  21. package/dist/checkbox/index.js +11 -9
  22. package/dist/checkbox/index.js.map +1 -1
  23. package/dist/combobox/index.js +4 -4
  24. package/dist/combobox/index.js.map +1 -1
  25. package/dist/combobox/primitives.d.ts +1 -1
  26. package/dist/combobox/primitives.js +5 -5
  27. package/dist/combobox/primitives.js.map +1 -1
  28. package/dist/dev/refresh.d.ts +1 -0
  29. package/dist/dev/refresh.js +2 -0
  30. package/dist/dev/refresh.js.map +1 -0
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.js +14 -14
  33. package/dist/input/index.js +9 -9
  34. package/dist/input/index.js.map +1 -1
  35. package/dist/jsx-runtime.js +2 -2
  36. package/dist/listbox/index.js +2 -2
  37. package/dist/menu/hover-aim.js.map +1 -1
  38. package/dist/menu/index.js +3 -3
  39. package/dist/menu/primitives.d.ts +1 -1
  40. package/dist/menu/primitives.js +6 -6
  41. package/dist/menu/primitives.js.map +1 -1
  42. package/dist/popover/index.js +3 -3
  43. package/dist/popover/scroll-lock.js.map +1 -1
  44. package/dist/radio/index.js +7 -7
  45. package/dist/radio/index.js.map +1 -1
  46. package/dist/runtime/client-entry-boundary.d.ts +14 -0
  47. package/dist/runtime/client-entry-boundary.js +29 -0
  48. package/dist/runtime/client-entry-boundary.js.map +1 -0
  49. package/dist/runtime/component.d.ts +19 -4
  50. package/dist/runtime/component.js +39 -9
  51. package/dist/runtime/component.js.map +1 -1
  52. package/dist/runtime/core/attributes.js +36 -3
  53. package/dist/runtime/core/attributes.js.map +1 -1
  54. package/dist/runtime/core/children.d.ts +2 -0
  55. package/dist/runtime/core/children.js +26 -2
  56. package/dist/runtime/core/children.js.map +1 -1
  57. package/dist/runtime/core/props.js +2 -2
  58. package/dist/runtime/core/vnode.d.ts +3 -3
  59. package/dist/runtime/core/vnode.js +22 -1
  60. package/dist/runtime/core/vnode.js.map +1 -1
  61. package/dist/runtime/create-element.d.ts +2 -2
  62. package/dist/runtime/create-element.js +3 -2
  63. package/dist/runtime/create-element.js.map +1 -1
  64. package/dist/runtime/diff-dom.js +307 -220
  65. package/dist/runtime/diff-dom.js.map +1 -1
  66. package/dist/runtime/diff-props.js +3 -3
  67. package/dist/runtime/dom.d.ts +12 -0
  68. package/dist/runtime/element-function.d.ts +3 -0
  69. package/dist/runtime/element-function.js +2 -0
  70. package/dist/runtime/element-function.js.map +1 -0
  71. package/dist/runtime/form-navigation.d.ts +25 -0
  72. package/dist/runtime/form-navigation.js +132 -0
  73. package/dist/runtime/form-navigation.js.map +1 -0
  74. package/dist/runtime/frame-resolution.d.ts +5 -0
  75. package/dist/runtime/frame-resolution.js +10 -0
  76. package/dist/runtime/frame-resolution.js.map +1 -0
  77. package/dist/runtime/frame.d.ts +59 -15
  78. package/dist/runtime/frame.js +383 -114
  79. package/dist/runtime/frame.js.map +1 -1
  80. package/dist/runtime/jsx.d.ts +1 -1
  81. package/dist/runtime/jsx.js +1 -1
  82. package/dist/runtime/jsx.js.map +1 -1
  83. package/dist/runtime/key.d.ts +1 -0
  84. package/dist/runtime/key.js +2 -0
  85. package/dist/runtime/key.js.map +1 -0
  86. package/dist/runtime/mixins/attrs-mixin.js +2 -2
  87. package/dist/runtime/mixins/link-mixin.js +4 -3
  88. package/dist/runtime/mixins/link-mixin.js.map +1 -1
  89. package/dist/runtime/mixins/mixin.d.ts +15 -10
  90. package/dist/runtime/mixins/mixin.js +4 -4
  91. package/dist/runtime/mixins/mixin.js.map +1 -1
  92. package/dist/runtime/mixins/on-mixin.js +1 -1
  93. package/dist/runtime/mixins/ref-mixin.js +1 -1
  94. package/dist/runtime/module-preloader.d.ts +8 -0
  95. package/dist/runtime/module-preloader.js +95 -0
  96. package/dist/runtime/module-preloader.js.map +1 -0
  97. package/dist/runtime/navigation.d.ts +4 -1
  98. package/dist/runtime/navigation.js +157 -32
  99. package/dist/runtime/navigation.js.map +1 -1
  100. package/dist/runtime/reconcile.d.ts +11 -10
  101. package/dist/runtime/reconcile.js +549 -421
  102. package/dist/runtime/reconcile.js.map +1 -1
  103. package/dist/runtime/refresh.d.ts +28 -0
  104. package/dist/runtime/refresh.js +31 -0
  105. package/dist/runtime/refresh.js.map +1 -0
  106. package/dist/runtime/render.js +1 -1
  107. package/dist/runtime/render.js.map +1 -1
  108. package/dist/runtime/run.d.ts +3 -1
  109. package/dist/runtime/run.js +15 -7
  110. package/dist/runtime/run.js.map +1 -1
  111. package/dist/runtime/scheduler.js +8 -16
  112. package/dist/runtime/scheduler.js.map +1 -1
  113. package/dist/runtime/to-vnode.d.ts +2 -2
  114. package/dist/runtime/to-vnode.js +68 -20
  115. package/dist/runtime/to-vnode.js.map +1 -1
  116. package/dist/runtime/vdom.d.ts +4 -2
  117. package/dist/runtime/vdom.js +87 -42
  118. package/dist/runtime/vdom.js.map +1 -1
  119. package/dist/runtime/vnode.d.ts +138 -67
  120. package/dist/runtime/vnode.js +17 -12
  121. package/dist/runtime/vnode.js.map +1 -1
  122. package/dist/select/index.js +11 -11
  123. package/dist/select/index.js.map +1 -1
  124. package/dist/select/primitives.d.ts +1 -1
  125. package/dist/select/primitives.js +6 -6
  126. package/dist/server/stream.d.ts +2 -0
  127. package/dist/server/stream.js +74 -29
  128. package/dist/server/stream.js.map +1 -1
  129. package/dist/shared/focus-styles.d.ts +1 -1
  130. package/dist/shared/focus-styles.js +1 -1
  131. package/dist/shared/focus-styles.js.map +1 -1
  132. package/dist/shared/listbox-popover-styles.js +1 -1
  133. package/dist/shared/style-values.js +26 -26
  134. package/dist/shared/style-values.js.map +1 -1
  135. package/dist/style/css-mixin.js +3 -3
  136. package/dist/style/index.js +2 -2
  137. package/dist/style/style.js +25 -6
  138. package/dist/style/style.js.map +1 -1
  139. package/dist/style/stylesheet.d.ts +0 -1
  140. package/dist/style/stylesheet.js +13 -54
  141. package/dist/style/stylesheet.js.map +1 -1
  142. package/dist/tabs/index.js +11 -11
  143. package/dist/tabs/index.js.map +1 -1
  144. package/dist/tabs/primitives.d.ts +1 -1
  145. package/dist/test.js +1 -1
  146. package/dist/toggle/index.js +9 -9
  147. package/dist/toggle/index.js.map +1 -1
  148. package/dist/toggle/primitives.d.ts +1 -1
  149. package/package.json +10 -7
  150. package/src/animation/animate-mixins.ts +3 -3
  151. package/src/animation/demos/color-interpolation.tsx +1 -1
  152. package/src/button/index.ts +24 -24
  153. package/src/checkbox/index.ts +10 -8
  154. package/src/combobox/index.tsx +1 -1
  155. package/src/dev/refresh.ts +1 -0
  156. package/src/index.ts +1 -1
  157. package/src/input/index.ts +8 -8
  158. package/src/radio/index.ts +6 -5
  159. package/src/runtime/client-entry-boundary.ts +54 -0
  160. package/src/runtime/component.ts +65 -16
  161. package/src/runtime/core/attributes.ts +35 -1
  162. package/src/runtime/core/children.ts +24 -2
  163. package/src/runtime/core/vnode.ts +25 -4
  164. package/src/runtime/create-element.ts +6 -5
  165. package/src/runtime/demos/readme.demo.tsx +4 -4
  166. package/src/runtime/diff-dom.ts +370 -244
  167. package/src/runtime/dom.ts +331 -214
  168. package/src/runtime/element-function.ts +1 -0
  169. package/src/runtime/event-listeners.ts +1 -1
  170. package/src/runtime/form-navigation.ts +184 -0
  171. package/src/runtime/frame-resolution.ts +13 -0
  172. package/src/runtime/frame.ts +500 -124
  173. package/src/runtime/jsx.ts +3 -5
  174. package/src/runtime/key.ts +1 -0
  175. package/src/runtime/mixins/link-mixin.ts +1 -0
  176. package/src/runtime/mixins/mixin.ts +21 -18
  177. package/src/runtime/module-preloader.ts +115 -0
  178. package/src/runtime/navigation.ts +204 -30
  179. package/src/runtime/reconcile.ts +762 -759
  180. package/src/runtime/refresh.ts +46 -0
  181. package/src/runtime/run.ts +12 -2
  182. package/src/runtime/scheduler.ts +9 -29
  183. package/src/runtime/to-vnode.ts +85 -23
  184. package/src/runtime/vdom.ts +89 -70
  185. package/src/runtime/vnode.ts +192 -88
  186. package/src/select/index.tsx +9 -9
  187. package/src/server/README.md +13 -0
  188. package/src/server/stream.ts +93 -30
  189. package/src/shared/focus-styles.ts +1 -1
  190. package/src/shared/style-values.ts +26 -26
  191. package/src/style/style.ts +25 -6
  192. package/src/style/stylesheet.ts +32 -57
  193. package/src/tabs/index.tsx +12 -11
  194. package/src/test/README.md +151 -60
  195. package/src/toggle/index.tsx +9 -7
@@ -0,0 +1,46 @@
1
+ import type { VirtualRoot } from './vdom.ts'
2
+
3
+ /**
4
+ * Reports whether an existing component instance must remount instead of reconciling during a
5
+ * development refresh.
6
+ *
7
+ * @param type Component function being considered for reuse.
8
+ * @returns `true` when existing instances must be remounted.
9
+ */
10
+ export type ComponentStalenessCheck = (type: Function) => boolean
11
+
12
+ export let componentStalenessCheck: ComponentStalenessCheck | null = null
13
+
14
+ const roots = new Set<VirtualRoot>()
15
+
16
+ /**
17
+ * Installs the process-wide component compatibility check used during development refreshes.
18
+ *
19
+ * When the check returns `true`, reconciliation replaces existing instances of that component
20
+ * instead of preserving their state. This is a low-level integration point for HMR runtimes.
21
+ *
22
+ * @param check Callback that returns `true` when instances of a component must be remounted.
23
+ */
24
+ export function setComponentStalenessCheck(check: ComponentStalenessCheck): void {
25
+ componentStalenessCheck = check
26
+ }
27
+
28
+ export function registerRoot(root: VirtualRoot): void {
29
+ roots.add(root)
30
+ }
31
+
32
+ export function unregisterRoot(root: VirtualRoot): void {
33
+ roots.delete(root)
34
+ }
35
+
36
+ /**
37
+ * Immediately schedules reconciliation for every active Remix UI root using its current element.
38
+ *
39
+ * HMR runtimes call this after installing updated component implementations so mounted trees render
40
+ * the new code while preserving compatible component state.
41
+ */
42
+ export function reconcileRoots(): void {
43
+ for (let root of roots) {
44
+ root.reconcile()
45
+ }
46
+ }
@@ -1,7 +1,7 @@
1
1
  import { createFrame, type Frame } from './frame.ts'
2
2
  import { createScheduler } from './vdom.ts'
3
3
  import { createStyleManager } from '../style/index.ts'
4
- import type { FrameHandle } from './component.ts'
4
+ import type { FrameHandle, Handle } from './component.ts'
5
5
  import { createComponentErrorEvent } from './error-event.ts'
6
6
  import type { ComponentErrorEvent } from './error-event.ts'
7
7
  import type { LoadModule, ResolveFrame } from './frame.ts'
@@ -40,6 +40,8 @@ export type AppRuntimeEventMap = {
40
40
  * Client runtime returned by {@link run}.
41
41
  */
42
42
  export type AppRuntime = TypedEventTarget<AppRuntimeEventMap> & {
43
+ /** Access top-level and named frames in the application runtime. */
44
+ frames: Handle['frames']
43
45
  /** Resolves after the current document finishes hydrating. */
44
46
  ready(): Promise<void>
45
47
  /** Flushes any queued component updates synchronously. */
@@ -99,18 +101,26 @@ export function run(init: RunInit): AppRuntime {
99
101
  })
100
102
 
101
103
  let appController = new AbortController()
102
- startNavigationListener(appController.signal)
104
+ let frames: Handle['frames'] = {
105
+ top: topFrame.handle,
106
+ get(name) {
107
+ return namedFrames.get(name)
108
+ },
109
+ }
110
+ startNavigationListener(appController.signal, init.resolveFrame !== undefined)
103
111
  let readyPromise = topFrame.ready().catch((error) => {
104
112
  errorTarget.dispatchEvent(createComponentErrorEvent(error))
105
113
  throw error
106
114
  })
107
115
 
108
116
  return Object.assign(errorTarget, {
117
+ frames,
109
118
  ready: () => readyPromise,
110
119
  flush: () => topFrame.flush(),
111
120
  dispose: () => {
112
121
  appController.abort()
113
122
  topFrame.dispose()
123
+ styleManager.dispose()
114
124
  },
115
125
  })
116
126
  }
@@ -1,6 +1,6 @@
1
1
  import { createDocumentState } from './document-state.ts'
2
2
  import { createComponentErrorEvent } from './error-event.ts'
3
- import type { CommittedComponentNode, VNode } from './vnode.ts'
3
+ import type { CommittedComponentNode, VNodeParent } from './vnode.ts'
4
4
  import { isCommittedComponentNode } from './vnode.ts'
5
5
  import {
6
6
  findNextSiblingDomAnchor,
@@ -87,11 +87,6 @@ export function createScheduler(
87
87
  }, 0)
88
88
  }
89
89
 
90
- function getFrameStyleManager(vnode: CommittedComponentNode): StyleManager {
91
- let runtime = vnode._handle?.frame.$runtime as { styleManager?: StyleManager } | undefined
92
- return runtime?.styleManager ?? styles
93
- }
94
-
95
90
  function flush() {
96
91
  if (flushing) return
97
92
  flushing = true
@@ -126,32 +121,18 @@ export function createScheduler(
126
121
 
127
122
  if (batch.size > 0) {
128
123
  let vnodes = Array.from(batch)
129
- let noScheduledAncestor = new Set<VNode>()
124
+ let noScheduledAncestor = new Set<VNodeParent>()
130
125
 
131
126
  for (let [vnode, domParent] of vnodes) {
132
127
  if (ancestorIsScheduled(vnode, batch, noScheduledAncestor)) continue
133
- let handle = vnode._handle
134
128
  let curr = vnode._content
135
- let vParent = vnode._parent!
136
129
  // Calculate anchor at render time from current vdom position (never stale).
137
130
  // Needed for fragment self-updates that add children - without this, new children
138
131
  // would be appended after siblings. The keyed diff has placement logic, but unkeyed
139
132
  // diff relies on anchor for correct positioning.
140
133
  let anchor = findNextSiblingDomAnchor(vnode) || undefined
141
134
  try {
142
- let updateStyles = getFrameStyleManager(vnode)
143
- renderComponent(
144
- handle,
145
- curr,
146
- vnode,
147
- domParent,
148
- handle.frame,
149
- scheduler,
150
- updateStyles,
151
- rootTarget,
152
- vParent,
153
- anchor,
154
- )
135
+ renderComponent(curr, vnode, domParent, vnode._context, anchor)
155
136
  } catch (error) {
156
137
  dispatchError(error)
157
138
  }
@@ -177,8 +158,7 @@ export function createScheduler(
177
158
 
178
159
  function dispatchPhaseEvent(type: SchedulerPhaseType, parents: ParentNode[]) {
179
160
  if (phaseListenerCounts[type] === 0) return
180
- let event = new Event(type) as SchedulerPhaseEvent
181
- event.parents = parents
161
+ let event: SchedulerPhaseEvent = Object.assign(new Event(type), { parents })
182
162
  phaseEvents.dispatchEvent(event)
183
163
  }
184
164
 
@@ -201,12 +181,12 @@ export function createScheduler(
201
181
  }
202
182
 
203
183
  function ancestorIsScheduled(
204
- vnode: VNode,
184
+ vnode: CommittedComponentNode,
205
185
  batch: Map<CommittedComponentNode, ParentNode>,
206
- safe: Set<VNode>,
186
+ safe: Set<VNodeParent>,
207
187
  ): boolean {
208
- let path: VNode[] = []
209
- let current = vnode._parent
188
+ let path: VNodeParent[] = []
189
+ let current: VNodeParent | undefined = vnode._parent
210
190
 
211
191
  while (current) {
212
192
  // Already verified this node has no scheduled ancestor above it
@@ -221,7 +201,7 @@ export function createScheduler(
221
201
  return true
222
202
  }
223
203
 
224
- current = current._parent
204
+ current = current.kind === 'root' ? undefined : current._parent
225
205
  }
226
206
 
227
207
  // Reached root - mark entire path as safe for future lookups
@@ -1,48 +1,110 @@
1
1
  import { Fragment } from './component.ts'
2
+ import { Frame } from './component.ts'
2
3
  import { invariant } from './invariant.ts'
3
- import { isEmptyChild, isPrimitiveChild, normalizeChildren } from './core/children.ts'
4
- import type { RemixElement, RemixNode } from './jsx.ts'
5
- import { isRemixElement, NON_RENDER_NODE, TEXT_NODE, type VNode } from './vnode.ts'
4
+ import { isEmptyChild, isPrimitiveChild, isRemixNode, normalizeChildren } from './core/children.ts'
5
+ import type { RemixNode } from './jsx.ts'
6
+ import type { ElementFunction } from './element-function.ts'
7
+ import type { FrameProps } from './component.ts'
8
+ import { isMixinDescriptor } from './mixins/mixin.ts'
9
+ import {
10
+ isRemixElement,
11
+ NON_RENDER_NODE,
12
+ TEXT_NODE,
13
+ type RuntimeElementProps,
14
+ type RuntimeHostProps,
15
+ type VNodeInput,
16
+ } from './vnode.ts'
6
17
 
7
- function flatMapChildrenToVNodes(node: RemixElement): VNode[] {
8
- if (!('children' in node.props)) return []
9
- let children = node.props.children
18
+ function flatMapChildrenToVNodes(props: RuntimeHostProps): VNodeInput[] {
19
+ let children = props.children
20
+ if (children === undefined) return []
10
21
  if (!Array.isArray(children)) return [toVNode(children)]
11
- let vnodes: VNode[] = []
22
+ let vnodes: VNodeInput[] = []
12
23
  flattenChildrenToVNodes(children, vnodes)
13
24
  return vnodes
14
25
  }
15
26
 
16
- function flattenChildrenToVNodes(nodes: RemixNode[], out: VNode[]): void {
17
- for (let child of normalizeChildren(nodes)) {
18
- out.push(toVNode(child))
27
+ function flattenChildrenToVNodes(nodes: RemixNode[], out: VNodeInput[]): void {
28
+ let children = normalizeChildren(nodes)
29
+ for (let i = 0; i < children.length; i++) {
30
+ out.push(toVNode(children[i]))
19
31
  }
20
32
  }
21
33
 
22
- export function toVNode(node: RemixNode): VNode {
34
+ export function toVNode(node: RemixNode): VNodeInput {
23
35
  if (isEmptyChild(node)) {
24
- return { type: NON_RENDER_NODE }
36
+ return { kind: 'empty', type: NON_RENDER_NODE }
25
37
  }
26
38
 
27
39
  if (isPrimitiveChild(node)) {
28
- return { type: TEXT_NODE, _text: String(node) }
40
+ return { kind: 'text', type: TEXT_NODE, _text: String(node) }
41
+ }
42
+
43
+ if (isRemixElement(node)) {
44
+ if (node.type === Fragment) {
45
+ let props = parseHostProps(node.props)
46
+ return {
47
+ kind: 'fragment',
48
+ type: Fragment,
49
+ key: node.key,
50
+ _children: flatMapChildrenToVNodes(props),
51
+ }
52
+ }
53
+
54
+ if (node.type === Frame) {
55
+ invariant(isFrameProps(node.props), '<Frame /> requires a src prop')
56
+ return { kind: 'frame', type: Frame, key: node.key, props: node.props }
57
+ }
58
+
59
+ if (typeof node.type === 'string') {
60
+ let props = parseHostProps(node.props)
61
+ // When innerHTML is set, ignore children
62
+ let children = props.innerHTML != null ? [] : flatMapChildrenToVNodes(props)
63
+ return {
64
+ kind: 'host',
65
+ type: node.type,
66
+ key: node.key,
67
+ props,
68
+ _children: children,
69
+ }
70
+ }
71
+ invariant(isElementFunction(node.type), 'Expected component element type')
72
+ return { kind: 'component', type: node.type, key: node.key, props: node.props }
29
73
  }
30
74
 
31
75
  if (Array.isArray(node)) {
32
- let children: VNode[] = []
76
+ let children: VNodeInput[] = []
33
77
  flattenChildrenToVNodes(node, children)
34
- return { type: Fragment, _children: children }
78
+ return { kind: 'fragment', type: Fragment, _children: children }
35
79
  }
36
80
 
37
- if (node.type === Fragment) {
38
- return { type: Fragment, key: node.key, _children: flatMapChildrenToVNodes(node) }
39
- }
81
+ invariant(false, 'Unexpected RemixNode')
82
+ }
40
83
 
41
- if (isRemixElement(node)) {
42
- // When innerHTML is set, ignore children
43
- let children = node.props.innerHTML != null ? [] : flatMapChildrenToVNodes(node)
44
- return { type: node.type, key: node.key, props: node.props, _children: children }
84
+ function isFrameProps(props: RuntimeElementProps): props is RuntimeElementProps & FrameProps {
85
+ return typeof props.src === 'string' && props.src.length > 0
86
+ }
87
+
88
+ function parseHostProps(props: RuntimeElementProps): RuntimeHostProps {
89
+ let children = props.children
90
+ invariant(children === undefined || isRemixNode(children), 'Invalid host children')
91
+
92
+ let innerHTML = props.innerHTML
93
+ invariant(innerHTML === undefined || typeof innerHTML === 'string', 'Invalid innerHTML prop')
94
+
95
+ let mix = props.mix
96
+ invariant(mix === undefined || isRuntimeMixValue(mix), 'Invalid mix prop')
97
+ return props
98
+ }
99
+
100
+ function isRuntimeMixValue(value: unknown): value is RuntimeHostProps['mix'] {
101
+ if (!Array.isArray(value)) return isMixinDescriptor(value)
102
+ for (let descriptor of value) {
103
+ if (!isMixinDescriptor(descriptor)) return false
45
104
  }
105
+ return true
106
+ }
46
107
 
47
- invariant(false, 'Unexpected RemixNode')
108
+ function isElementFunction(value: unknown): value is ElementFunction {
109
+ return typeof value === 'function'
48
110
  }
@@ -1,7 +1,8 @@
1
- import type { FrameContent, FrameHandle } from './component.ts'
1
+ import type { FrameHandle } from './component.ts'
2
2
  import { createFrameHandle } from './component.ts'
3
3
  import { invariant } from './invariant.ts'
4
4
  import type { RemixNode } from './jsx.ts'
5
+ import { createFrameRuntime, type ResolveFrame } from './frame.ts'
5
6
  import {
6
7
  createComponentErrorEvent,
7
8
  getComponentError,
@@ -11,8 +12,9 @@ import { createScheduler, type Scheduler } from './scheduler.ts'
11
12
  import { diffVNodes, remove as removeVNode } from './reconcile.ts'
12
13
  import { toVNode } from './to-vnode.ts'
13
14
  import { TypedEventTarget } from './typed-event-target.ts'
14
- import { ROOT_VNODE, type VNode } from './vnode.ts'
15
+ import { ROOT_VNODE, type CommittedVNode, type ReconcileContext, type RootVNode } from './vnode.ts'
15
16
  import { resetStyleState, defaultStyleManager } from './diff-props.ts'
17
+ import { registerRoot, unregisterRoot } from './refresh.ts'
16
18
  import type { StyleManager } from '../style/index.ts'
17
19
 
18
20
  /**
@@ -27,6 +29,7 @@ export type VirtualRootEventMap = {
27
29
  */
28
30
  export type VirtualRoot = TypedEventTarget<VirtualRootEventMap> & {
29
31
  render: (element: RemixNode) => void
32
+ reconcile: () => void
30
33
  dispose: () => void
31
34
  flush: () => void
32
35
  }
@@ -40,11 +43,7 @@ export type VirtualRootOptions = {
40
43
  styleManager?: StyleManager
41
44
  frameInit?: {
42
45
  src?: string
43
- resolveFrame: (
44
- src: string,
45
- signal?: AbortSignal,
46
- target?: string,
47
- ) => Promise<FrameContent> | FrameContent
46
+ resolveFrame: ResolveFrame
48
47
  loadModule?: (moduleUrl: string, exportName: string) => Promise<Function> | Function
49
48
  }
50
49
  }
@@ -73,7 +72,8 @@ export function createRangeRoot(
73
72
  options: VirtualRootOptions = {},
74
73
  ): VirtualRoot {
75
74
  let [start, end] = boundaries
76
- let vroot: VNode | null = null
75
+ let vroot: CommittedVNode | null = null
76
+ let currentElement: RemixNode | undefined
77
77
  let styles = options.styleManager ?? defaultStyleManager
78
78
 
79
79
  let container = end.parentNode
@@ -96,6 +96,12 @@ export function createRangeRoot(
96
96
  scheduler,
97
97
  styleManager: styles,
98
98
  })
99
+ let context: ReconcileContext = {
100
+ frame: frameStub,
101
+ scheduler,
102
+ styles,
103
+ rootTarget: eventTarget,
104
+ }
99
105
 
100
106
  let isErrorForwardingAttached = false
101
107
  function forwardDomError(event: Event) {
@@ -113,13 +119,16 @@ export function createRangeRoot(
113
119
  }
114
120
  attachDomErrorForwarding()
115
121
 
116
- return Object.assign(eventTarget, {
122
+ let root = Object.assign(eventTarget, {
117
123
  render(element: RemixNode) {
118
124
  attachDomErrorForwarding()
125
+ currentElement = element
119
126
 
120
127
  let vnode = toVNode(element)
121
- let vParent: VNode = {
128
+ let vParent: RootVNode = {
129
+ kind: 'root',
122
130
  type: ROOT_VNODE,
131
+ _children: [],
123
132
  _svg: false,
124
133
  _rangeStart: start,
125
134
  _rangeEnd: end,
@@ -127,32 +136,30 @@ export function createRangeRoot(
127
136
  }
128
137
  scheduler.enqueueWork([
129
138
  () => {
130
- diffVNodes(
131
- vroot,
132
- vnode,
133
- parent,
134
- frameStub,
135
- scheduler,
136
- styles,
137
- vParent,
138
- eventTarget,
139
- end,
140
- hydrationCursor,
141
- )
142
- vroot = vnode
139
+ let cursor = hydrationCursor === null ? undefined : { current: hydrationCursor }
140
+ let committed = diffVNodes(vroot, vnode, parent, vParent, context, end, cursor)
141
+ vParent._children = [committed]
142
+ vroot = committed
143
143
  hydrationCursor = null
144
144
  },
145
145
  ])
146
146
  scheduler.dequeue()
147
147
  },
148
148
 
149
+ reconcile() {
150
+ if (currentElement === undefined) return
151
+ root.render(currentElement)
152
+ },
153
+
149
154
  dispose() {
150
155
  detachDomErrorForwarding()
156
+ unregisterRoot(root)
157
+ currentElement = undefined
151
158
 
152
159
  if (!vroot) return
153
160
  let current = vroot
154
161
  vroot = null
155
- scheduler.enqueueWork([() => removeVNode(current, parent, scheduler, styles)])
162
+ scheduler.enqueueWork([() => removeVNode(current, parent, context)])
156
163
  scheduler.dequeue()
157
164
  },
158
165
 
@@ -160,6 +167,9 @@ export function createRangeRoot(
160
167
  scheduler.dequeue()
161
168
  },
162
169
  })
170
+
171
+ registerRoot(root)
172
+ return root
163
173
  }
164
174
 
165
175
  /**
@@ -170,10 +180,14 @@ export function createRangeRoot(
170
180
  * @returns A virtual root controller.
171
181
  */
172
182
  export function createRoot(container: HTMLElement, options: VirtualRootOptions = {}): VirtualRoot {
173
- let vroot: VNode | null = null
183
+ let vroot: CommittedVNode | null = null
184
+ let currentElement: RemixNode | undefined
174
185
  let styles = options.styleManager ?? defaultStyleManager
175
186
  if (container.innerHTML.trim() !== '') {
176
- styles.replaceServerStyles(container)
187
+ // Adopt additively: multiple roots hydrating separate islands may share
188
+ // the default style manager, and adopting a later island must not release
189
+ // the server styles an earlier island still depends on.
190
+ styles.adoptServerStyles(container)
177
191
  }
178
192
  let hydrationCursor = container.innerHTML.trim() !== '' ? container.firstChild : undefined
179
193
 
@@ -190,6 +204,12 @@ export function createRoot(container: HTMLElement, options: VirtualRootOptions =
190
204
  scheduler,
191
205
  styleManager: styles,
192
206
  })
207
+ let context: ReconcileContext = {
208
+ frame: frameStub,
209
+ scheduler,
210
+ styles,
211
+ rootTarget: eventTarget,
212
+ }
193
213
 
194
214
  let isErrorForwardingAttached = false
195
215
  function forwardDomError(event: Event) {
@@ -207,40 +227,44 @@ export function createRoot(container: HTMLElement, options: VirtualRootOptions =
207
227
  }
208
228
  attachDomErrorForwarding()
209
229
 
210
- return Object.assign(eventTarget, {
230
+ let root = Object.assign(eventTarget, {
211
231
  render(element: RemixNode) {
212
232
  attachDomErrorForwarding()
233
+ currentElement = element
213
234
 
214
235
  let vnode = toVNode(element)
215
- let vParent: VNode = { type: ROOT_VNODE, _svg: false }
236
+ let vParent: RootVNode = {
237
+ kind: 'root',
238
+ type: ROOT_VNODE,
239
+ _children: [],
240
+ _svg: false,
241
+ }
216
242
  scheduler.enqueueWork([
217
243
  () => {
218
- diffVNodes(
219
- vroot,
220
- vnode,
221
- container,
222
- frameStub,
223
- scheduler,
224
- styles,
225
- vParent,
226
- eventTarget,
227
- undefined,
228
- hydrationCursor,
229
- )
230
- vroot = vnode
244
+ let cursor = hydrationCursor === undefined ? undefined : { current: hydrationCursor }
245
+ let committed = diffVNodes(vroot, vnode, container, vParent, context, undefined, cursor)
246
+ vParent._children = [committed]
247
+ vroot = committed
231
248
  hydrationCursor = undefined
232
249
  },
233
250
  ])
234
251
  scheduler.dequeue()
235
252
  },
236
253
 
254
+ reconcile() {
255
+ if (currentElement === undefined) return
256
+ root.render(currentElement)
257
+ },
258
+
237
259
  dispose() {
238
260
  detachDomErrorForwarding()
261
+ unregisterRoot(root)
262
+ currentElement = undefined
239
263
 
240
264
  if (!vroot) return
241
265
  let current = vroot
242
266
  vroot = null
243
- scheduler.enqueueWork([() => removeVNode(current, container, scheduler, styles)])
267
+ scheduler.enqueueWork([() => removeVNode(current, container, context)])
244
268
  scheduler.dequeue()
245
269
  },
246
270
 
@@ -248,15 +272,14 @@ export function createRoot(container: HTMLElement, options: VirtualRootOptions =
248
272
  scheduler.dequeue()
249
273
  },
250
274
  })
275
+
276
+ registerRoot(root)
277
+ return root
251
278
  }
252
279
 
253
280
  function createRootFrameHandle(init: {
254
281
  src?: string
255
- resolveFrame?: (
256
- src: string,
257
- signal?: AbortSignal,
258
- target?: string,
259
- ) => Promise<FrameContent> | FrameContent
282
+ resolveFrame?: ResolveFrame
260
283
  loadModule?: (moduleUrl: string, exportName: string) => Promise<Function> | Function
261
284
  errorTarget: EventTarget
262
285
  scheduler: Scheduler
@@ -270,29 +293,25 @@ function createRootFrameHandle(init: {
270
293
  )
271
294
  })
272
295
 
273
- let frame = createFrameHandle({
274
- src: init.src ?? '/',
275
- $runtime: {
276
- canResolveFrames: !!init.resolveFrame,
277
- topFrame: undefined,
278
- loadModule:
279
- init.loadModule ??
280
- (() => {
281
- throw new Error('loadModule is required to hydrate client entries inside <Frame />')
282
- }),
283
- resolveFrame,
284
- errorTarget: init.errorTarget,
285
- pendingClientEntries: new Map(),
286
- scheduler: init.scheduler,
287
- styleManager: init.styleManager,
288
- data: {},
289
- moduleCache: new Map(),
290
- moduleLoads: new Map(),
291
- frameInstances: new WeakMap(),
292
- namedFrames: new Map(),
293
- },
296
+ let runtime = createFrameRuntime({
297
+ topFrame: undefined,
298
+ loadModule:
299
+ init.loadModule ??
300
+ (() => {
301
+ throw new Error('loadModule is required to hydrate client entries inside <Frame />')
302
+ }),
303
+ resolveFrame,
304
+ errorTarget: init.errorTarget,
305
+ pendingClientEntries: new Map(),
306
+ scheduler: init.scheduler,
307
+ styleManager: init.styleManager,
308
+ moduleCache: new Map(),
309
+ moduleLoads: new Map(),
310
+ frameInstances: new WeakMap(),
311
+ namedFrames: new Map(),
294
312
  })
295
- let runtime = frame.$runtime as { topFrame?: FrameHandle } | undefined
296
- if (runtime) runtime.topFrame = frame
313
+ runtime.canResolveFrames = !!init.resolveFrame
314
+ let frame = createFrameHandle({ src: init.src ?? '/', $runtime: runtime })
315
+ runtime.topFrame = frame
297
316
  return frame
298
317
  }