@remix-run/ui 0.7.0 → 0.9.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 (91) hide show
  1. package/README.md +90 -14
  2. package/dist/animation/demos/drag-release.js +5 -7
  3. package/dist/animation/demos/drag-release.js.map +1 -1
  4. package/dist/index.d.ts +2 -2
  5. package/dist/index.js +1 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/runtime/component.d.ts +9 -4
  8. package/dist/runtime/component.js +38 -9
  9. package/dist/runtime/component.js.map +1 -1
  10. package/dist/runtime/core/mix.d.ts +21 -0
  11. package/dist/runtime/core/mix.js +128 -0
  12. package/dist/runtime/core/mix.js.map +1 -0
  13. package/dist/runtime/diff-dom.js +15 -16
  14. package/dist/runtime/diff-dom.js.map +1 -1
  15. package/dist/runtime/document-reload.d.ts +2 -0
  16. package/dist/runtime/document-reload.js +14 -0
  17. package/dist/runtime/document-reload.js.map +1 -0
  18. package/dist/runtime/dom.d.ts +21 -11
  19. package/dist/runtime/event-types.d.ts +14 -0
  20. package/dist/runtime/event-types.js +2 -0
  21. package/dist/runtime/event-types.js.map +1 -0
  22. package/dist/runtime/frame-resolution.js +8 -0
  23. package/dist/runtime/frame-resolution.js.map +1 -1
  24. package/dist/runtime/frame.d.ts +24 -6
  25. package/dist/runtime/frame.js +187 -60
  26. package/dist/runtime/frame.js.map +1 -1
  27. package/dist/runtime/import-map-manager.d.ts +8 -0
  28. package/dist/runtime/import-map-manager.js +295 -0
  29. package/dist/runtime/import-map-manager.js.map +1 -0
  30. package/dist/runtime/mixins/link-mixin.js +4 -4
  31. package/dist/runtime/mixins/link-mixin.js.map +1 -1
  32. package/dist/runtime/mixins/mixin.d.ts +9 -4
  33. package/dist/runtime/mixins/mixin.js +18 -132
  34. package/dist/runtime/mixins/mixin.js.map +1 -1
  35. package/dist/runtime/mixins/on-mixin.d.ts +1 -1
  36. package/dist/runtime/module-preloader.d.ts +2 -1
  37. package/dist/runtime/module-preloader.js +6 -5
  38. package/dist/runtime/module-preloader.js.map +1 -1
  39. package/dist/runtime/navigation.js +211 -34
  40. package/dist/runtime/navigation.js.map +1 -1
  41. package/dist/runtime/reconcile.js +18 -3
  42. package/dist/runtime/reconcile.js.map +1 -1
  43. package/dist/runtime/run.d.ts +3 -0
  44. package/dist/runtime/run.js +8 -4
  45. package/dist/runtime/run.js.map +1 -1
  46. package/dist/runtime/scheduler.js +37 -11
  47. package/dist/runtime/scheduler.js.map +1 -1
  48. package/dist/runtime/spa-response.d.ts +30 -0
  49. package/dist/runtime/spa-response.js +47 -0
  50. package/dist/runtime/spa-response.js.map +1 -0
  51. package/dist/runtime/to-vnode.js +1 -1
  52. package/dist/runtime/to-vnode.js.map +1 -1
  53. package/dist/runtime/typed-event-target.d.ts +0 -4
  54. package/dist/runtime/typed-event-target.js.map +1 -1
  55. package/dist/server/stream.d.ts +25 -2
  56. package/dist/server/stream.js +382 -171
  57. package/dist/server/stream.js.map +1 -1
  58. package/dist/style/stylesheet.js +2 -2
  59. package/dist/style/stylesheet.js.map +1 -1
  60. package/package.json +1 -1
  61. package/src/animation/demos/drag-release.ts +5 -7
  62. package/src/index.ts +2 -2
  63. package/src/runtime/component.ts +52 -14
  64. package/src/runtime/core/mix.ts +157 -0
  65. package/src/runtime/demos/readme.demo.tsx +7 -17
  66. package/src/runtime/diff-dom.ts +13 -15
  67. package/src/runtime/document-reload.ts +14 -0
  68. package/src/runtime/dom.ts +21 -11
  69. package/src/runtime/event-types.ts +27 -0
  70. package/src/runtime/frame-resolution.ts +9 -0
  71. package/src/runtime/frame.ts +216 -65
  72. package/src/runtime/import-map-manager.ts +369 -0
  73. package/src/runtime/mixins/link-mixin.ts +4 -4
  74. package/src/runtime/mixins/mixin.ts +44 -149
  75. package/src/runtime/mixins/on-mixin.ts +1 -1
  76. package/src/runtime/module-preloader.ts +9 -6
  77. package/src/runtime/navigation.ts +257 -42
  78. package/src/runtime/reconcile.ts +20 -4
  79. package/src/runtime/run.ts +13 -4
  80. package/src/runtime/scheduler.ts +53 -13
  81. package/src/runtime/spa-response.ts +56 -0
  82. package/src/runtime/to-vnode.ts +1 -1
  83. package/src/runtime/typed-event-target.ts +1 -6
  84. package/src/server/README.md +25 -5
  85. package/src/server/stream.ts +506 -202
  86. package/src/style/stylesheet.ts +3 -3
  87. package/src/test/utils.ts +1 -6
  88. package/dist/runtime/event-listeners.d.ts +0 -50
  89. package/dist/runtime/event-listeners.js +0 -31
  90. package/dist/runtime/event-listeners.js.map +0 -1
  91. package/src/runtime/event-listeners.ts +0 -171
@@ -10,7 +10,7 @@ import { createRangeRoot, createRoot } from './vdom.ts'
10
10
  import { diffNodes } from './diff-dom.ts'
11
11
  import { createStyleManager, type StyleManager } from '../style/index.ts'
12
12
  import { findFlushMarker, type FlushKind } from './stream-protocol.ts'
13
- import { getDocumentModulePreloader } from './module-preloader.ts'
13
+ import { getDocumentModulePreloader, type ProcessClientEntryPreloads } from './module-preloader.ts'
14
14
  import { unwrapFrameResolution } from './frame-resolution.ts'
15
15
  import {
16
16
  disposeClientEntryBoundary,
@@ -18,6 +18,8 @@ import {
18
18
  setClientEntryBoundaryOwner,
19
19
  type ClientEntryIdentity,
20
20
  } from './client-entry-boundary.ts'
21
+ import { getDocumentImportMapManager } from './import-map-manager.ts'
22
+ import { reloadDocument } from './document-reload.ts'
21
23
 
22
24
  type FrameRoot = [Comment, Comment] | Element | Document | DocumentFragment
23
25
 
@@ -104,6 +106,12 @@ type FrameReloadResult = {
104
106
  redirectedTo?: string
105
107
  }
106
108
 
109
+ type FrameReloadTransition = {
110
+ signal: AbortSignal
111
+ committed: Promise<void>
112
+ finished: Promise<FrameReloadResult>
113
+ }
114
+
107
115
  type FrameTemplateListener = (fragment: DocumentFragment) => void
108
116
 
109
117
  const bufferedFrameTemplates = new Map<string, DocumentFragment[]>()
@@ -170,8 +178,15 @@ export type FrameRuntime = {
170
178
  moduleLoads: Map<string, Promise<ElementFunction | undefined>>
171
179
  frameInstances: WeakMap<Comment, Frame>
172
180
  namedFrames: Map<string, FrameHandle>
173
- serverFrameReload: { signal: AbortSignal } | undefined
174
- reloadForNavigation?: (options?: FrameReloadOptions) => Promise<FrameReloadResult>
181
+ processClientEntryPreloads?: ProcessClientEntryPreloads
182
+ serverFrameReload:
183
+ | {
184
+ signal: AbortSignal
185
+ reconciliationTracker?: ReconciliationTracker
186
+ blockingFrameTracker?: ReconciliationTracker
187
+ }
188
+ | undefined
189
+ reloadForNavigation?: (options?: FrameReloadOptions) => FrameReloadTransition
175
190
  }
176
191
 
177
192
  export function isFrameRuntime(value: unknown): value is FrameRuntime {
@@ -188,7 +203,7 @@ export function isFrameRuntime(value: unknown): value is FrameRuntime {
188
203
  export function reloadFrameForNavigation(
189
204
  frame: FrameHandle,
190
205
  options?: FrameReloadOptions,
191
- ): Promise<FrameReloadResult> {
206
+ ): FrameReloadTransition {
192
207
  let runtime = frame.$runtime
193
208
  invariant(isFrameRuntime(runtime), 'Expected a frame runtime')
194
209
  let reload = runtime.reloadForNavigation
@@ -210,11 +225,14 @@ export type FrameContext = {
210
225
  moduleLoads: Map<string, Promise<ElementFunction | undefined>>
211
226
  frameInstances: WeakMap<Comment, Frame>
212
227
  namedFrames: Map<string, FrameHandle>
228
+ processClientEntryPreloads?: ProcessClientEntryPreloads
213
229
  lifecycleSignal: AbortSignal
214
230
  regionTailRef?: ChildNode | null
215
231
  regionParent?: ParentNode | null
216
232
  signal?: AbortSignal
217
- isActiveModulePreload?: (node: Node) => boolean
233
+ shouldPreserveHeadNode?: (node: Node) => boolean
234
+ reconciliationTracker?: ReconciliationTracker
235
+ blockingFrameTracker?: ReconciliationTracker
218
236
  }
219
237
 
220
238
  type FrameInit = {
@@ -233,6 +251,7 @@ type FrameInit = {
233
251
  moduleLoads: Map<string, Promise<ElementFunction | undefined>>
234
252
  frameInstances: WeakMap<Comment, Frame>
235
253
  namedFrames: Map<string, FrameHandle>
254
+ processClientEntryPreloads?: ProcessClientEntryPreloads
236
255
  }
237
256
 
238
257
  export type Frame = {
@@ -259,8 +278,11 @@ export type Frame = {
259
278
  }
260
279
 
261
280
  type RenderOptions = {
281
+ documentHref?: string
262
282
  flushKind?: FlushKind
263
- initialHydrationTracker?: InitialHydrationTracker
283
+ reconciliationTracker?: ReconciliationTracker
284
+ blockingFrameTracker?: ReconciliationTracker
285
+ onCommit?: () => void
264
286
  signal?: AbortSignal
265
287
  contentStatus?: 'pending' | 'resolved'
266
288
  data?: RmxData
@@ -281,6 +303,7 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
281
303
  let reloadKind: 'direct' | 'ancestor' | undefined
282
304
  let styleManager = init.styleManager ?? createStyleManager()
283
305
  let modulePreloader = getDocumentModulePreloader(container.doc)
306
+ let importMapManager = getDocumentImportMapManager(container.doc)
284
307
  let currentMarker = init.marker
285
308
  let displayedContentStatus: 'pending' | 'resolved' = init.marker?.status ?? 'resolved'
286
309
  let pendingTemplateMarkerId: string | undefined
@@ -291,16 +314,42 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
291
314
  let disposed = false
292
315
  let lifecycleController = new AbortController()
293
316
 
317
+ async function consumeClientEntryResources(
318
+ source: ParentNode,
319
+ documentHref?: string,
320
+ ): Promise<boolean> {
321
+ let importMapStatus = importMapManager.consumeImportMaps(source)
322
+ if (importMapStatus !== 'ready') {
323
+ lifecycleController.abort()
324
+ if (importMapStatus === 'conflict') reloadDocument(container.doc, documentHref)
325
+ return false
326
+ }
327
+ await modulePreloader.consumePreloadLinks(source, init.processClientEntryPreloads)
328
+ return true
329
+ }
330
+
331
+ let initialClientEntryResources: Promise<boolean> | undefined
332
+ function shouldPreserveManagedHeadNode(node: Node): boolean {
333
+ return (
334
+ importMapManager.shouldPreserveHeadNode(node) ||
335
+ (modulePreloader.hasActivePreloads() && modulePreloader.isActivePreload(node))
336
+ )
337
+ }
338
+
294
339
  if (isDocumentNode(container.root)) {
295
340
  modulePreloader.adoptInitialPreloadLinks(container.root)
296
341
  } else {
297
- modulePreloader.consumePreloadLinks(container.root)
342
+ initialClientEntryResources = consumeClientEntryResources(container.root)
298
343
  }
299
344
 
300
345
  // Merge any rmx-data found in the current document once at startup.
301
346
  mergeRmxDataFromDocument(init.data, container.doc)
302
347
 
303
- let runtime = createFrameRuntime({ ...init, styleManager, reloadForNavigation: reload })
348
+ let runtime = createFrameRuntime({
349
+ ...init,
350
+ styleManager,
351
+ reloadForNavigation: startReloadTransition,
352
+ })
304
353
 
305
354
  let frame = createFrameHandle({
306
355
  src: init.src,
@@ -331,6 +380,7 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
331
380
  moduleLoads: init.moduleLoads,
332
381
  frameInstances: init.frameInstances,
333
382
  namedFrames: init.namedFrames,
383
+ processClientEntryPreloads: init.processClientEntryPreloads,
334
384
  lifecycleSignal: lifecycleController.signal,
335
385
  regionTailRef: container.regionTailRef,
336
386
  regionParent: container.regionParent,
@@ -385,7 +435,24 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
385
435
  }
386
436
 
387
437
  if (isRenderAborted(options.signal)) return
388
- contentRoot.render(content)
438
+ let previousServerFrameReload = runtime.serverFrameReload
439
+ if (options.signal) {
440
+ runtime.serverFrameReload = {
441
+ signal: options.signal,
442
+ reconciliationTracker: options.reconciliationTracker,
443
+ blockingFrameTracker: options.blockingFrameTracker,
444
+ }
445
+ }
446
+
447
+ try {
448
+ contentRoot.render(content)
449
+ await new Promise<void>((resolve) => context.scheduler.enqueueCommitPhase([resolve]))
450
+ options.onCommit?.()
451
+ } finally {
452
+ runtime.serverFrameReload = previousServerFrameReload
453
+ }
454
+
455
+ if (isRenderAborted(options.signal)) return
389
456
  displayedContentStatus = options.contentStatus ?? 'resolved'
390
457
  return
391
458
  }
@@ -416,10 +483,16 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
416
483
 
417
484
  if (isFullDocumentReload && htmlContent !== undefined) {
418
485
  let parsed = new DOMParser().parseFromString(htmlContent, 'text/html')
419
- modulePreloader.consumePreloadLinks(parsed)
486
+ if (!(await consumeClientEntryResources(parsed, options.documentHref))) return
487
+ if (isRenderAborted(options.signal)) return
420
488
  let responseData = options.data
421
489
  mergeRmxDataFromDocument(responseData, parsed)
422
- let responseContext = { ...context, data: responseData }
490
+ let responseContext = {
491
+ ...context,
492
+ data: responseData,
493
+ reconciliationTracker: options.reconciliationTracker,
494
+ blockingFrameTracker: options.blockingFrameTracker,
495
+ }
423
496
  context.styleManager.adoptServerStyles(
424
497
  collectFrameServerStyleTags(createElementContainer(parsed)),
425
498
  )
@@ -431,9 +504,7 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
431
504
  regionParent: container.doc.documentElement,
432
505
  regionTailRef: null,
433
506
  signal: options.signal,
434
- isActiveModulePreload: modulePreloader.hasActivePreloads()
435
- ? modulePreloader.isActivePreload
436
- : undefined,
507
+ shouldPreserveHeadNode: shouldPreserveManagedHeadNode,
437
508
  })
438
509
  diffNodes([container.doc.body], [parsed.body], {
439
510
  ...responseContext,
@@ -447,10 +518,12 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
447
518
  scheduleHydrationInContainer(
448
519
  bodyContainer,
449
520
  responseContext,
450
- options.initialHydrationTracker,
521
+ options.reconciliationTracker,
451
522
  options.signal,
452
523
  )
453
- await createSubFrames(bodyContainer.childNodes, responseContext, options)
524
+ let subFramesReady = createSubFrames(bodyContainer.childNodes, responseContext, options)
525
+ options.onCommit?.()
526
+ await subFramesReady
454
527
  if (isRenderAborted(options.signal)) return
455
528
  displayedContentStatus = options.contentStatus ?? 'resolved'
456
529
  return
@@ -458,14 +531,20 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
458
531
 
459
532
  let fragment =
460
533
  htmlContent !== undefined ? createFragmentFromString(container.doc, htmlContent) : content
461
- modulePreloader.consumePreloadLinks(fragment)
534
+ if (!(await consumeClientEntryResources(fragment, options.documentHref))) return
535
+ if (isRenderAborted(options.signal)) return
462
536
  context.styleManager.adoptServerStyles(
463
537
  collectFrameServerStyleTags(createElementContainer(fragment)),
464
538
  )
465
539
  removeEmptyHeads(fragment)
466
540
  let responseData = options.data
467
541
  mergeRmxDataFromFragment(responseData, fragment)
468
- let responseContext = { ...context, data: responseData }
542
+ let responseContext = {
543
+ ...context,
544
+ data: responseData,
545
+ reconciliationTracker: options.reconciliationTracker,
546
+ blockingFrameTracker: options.blockingFrameTracker,
547
+ }
469
548
 
470
549
  let nextContainer = createContainer(fragment)
471
550
 
@@ -478,14 +557,15 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
478
557
  signal: options.signal,
479
558
  })
480
559
 
481
- if (isRenderAborted(options.signal)) return
482
560
  scheduleHydrationInContainer(
483
561
  container,
484
562
  responseContext,
485
- options.initialHydrationTracker,
563
+ options.reconciliationTracker,
486
564
  options.signal,
487
565
  )
488
- await createSubFrames(container.childNodes, responseContext, options)
566
+ let subFramesReady = createSubFrames(container.childNodes, responseContext, options)
567
+ options.onCommit?.()
568
+ await subFramesReady
489
569
  if (isRenderAborted(options.signal)) return
490
570
  displayedContentStatus = options.contentStatus ?? 'resolved'
491
571
  }
@@ -532,11 +612,13 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
532
612
  }
533
613
 
534
614
  async function hydrateInitial(): Promise<void> {
535
- let initialHydrationTracker = createInitialHydrationTracker()
615
+ let reconciliationTracker = createReconciliationTracker()
536
616
 
617
+ if ((await initialClientEntryResources) === false) return
618
+ if (disposed || context.lifecycleSignal.aborted) return
537
619
  context.styleManager.adoptServerStyles(collectFrameServerStyleTags(container))
538
620
  let subFramesReady = createSubFrames(container.childNodes, context)
539
- scheduleHydrationInContainer(container, context, initialHydrationTracker)
621
+ scheduleHydrationInContainer(container, context, reconciliationTracker)
540
622
 
541
623
  try {
542
624
  await subFramesReady
@@ -544,11 +626,11 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
544
626
  if (disposed || context.lifecycleSignal.aborted) return
545
627
 
546
628
  if (currentMarker?.status === 'pending') {
547
- await watchPendingFrameTemplate(currentMarker, initialHydrationTracker)
629
+ await watchPendingFrameTemplate(currentMarker, reconciliationTracker)
548
630
  }
549
631
 
550
- initialHydrationTracker.finalize()
551
- await initialHydrationTracker.ready()
632
+ reconciliationTracker.finalize()
633
+ await reconciliationTracker.ready()
552
634
  } finally {
553
635
  clearRmxData(context.data)
554
636
  }
@@ -615,7 +697,7 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
615
697
  if (marker.status === 'pending') {
616
698
  await watchPendingFrameTemplate(
617
699
  marker,
618
- options?.initialHydrationTracker,
700
+ options?.reconciliationTracker,
619
701
  options?.signal,
620
702
  isInheritedReload
621
703
  ? () => {
@@ -659,8 +741,25 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
659
741
  }
660
742
 
661
743
  async function reload(options?: FrameReloadOptions): Promise<FrameReloadResult> {
744
+ let transition = startReloadTransition(options)
745
+ void transition.committed.catch(() => {})
746
+ return await transition.finished
747
+ }
748
+
749
+ function startReloadTransition(options?: FrameReloadOptions): FrameReloadTransition {
662
750
  let controller = startReload(options?.signal)
663
- return await resolveAndRenderReload(controller, options)
751
+ let committed = Promise.withResolvers<void>()
752
+ let commitStarted = false
753
+ let finished = resolveAndRenderReload(controller, options, (ready) => {
754
+ if (commitStarted) return
755
+ commitStarted = true
756
+ void ready.then(committed.resolve, committed.reject)
757
+ })
758
+
759
+ // Settle committed when a reload is aborted or fails before rendering any content.
760
+ void finished.then(() => committed.resolve(), committed.reject)
761
+
762
+ return { signal: controller.signal, committed: committed.promise, finished }
664
763
  }
665
764
 
666
765
  function startReload(signal?: AbortSignal): AbortController {
@@ -722,6 +821,7 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
722
821
  async function resolveAndRenderReload(
723
822
  controller: AbortController,
724
823
  options?: FrameReloadOptions,
824
+ resolveCommit?: (ready: Promise<void>) => void,
725
825
  ): Promise<FrameReloadResult> {
726
826
  try {
727
827
  let resolution = await init.resolveFrame(frame.src, {
@@ -736,7 +836,23 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
736
836
  if (reloadController !== controller || controller.signal.aborted) {
737
837
  return { signal: controller.signal }
738
838
  }
739
- await render(content, { signal: controller.signal })
839
+ let reconciliationTracker = createReconciliationTracker()
840
+ let blockingFrameTracker = createReconciliationTracker()
841
+ let commitStarted = false
842
+ await render(content, {
843
+ documentHref: isDocumentNode(container.root) ? (redirectedTo ?? frame.src) : undefined,
844
+ signal: controller.signal,
845
+ reconciliationTracker,
846
+ blockingFrameTracker,
847
+ onCommit() {
848
+ if (commitStarted) return
849
+ commitStarted = true
850
+ blockingFrameTracker.finalize()
851
+ resolveCommit?.(blockingFrameTracker.ready())
852
+ },
853
+ })
854
+ reconciliationTracker.finalize()
855
+ await reconciliationTracker.ready()
740
856
  return {
741
857
  signal: controller.signal,
742
858
  redirectedTo:
@@ -825,7 +941,7 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
825
941
 
826
942
  async function watchPendingFrameTemplate(
827
943
  marker: FrameMarkerData,
828
- initialHydrationTracker?: InitialHydrationTracker,
944
+ reconciliationTracker?: ReconciliationTracker,
829
945
  signal?: AbortSignal,
830
946
  onResolved?: () => void,
831
947
  ): Promise<void> {
@@ -838,7 +954,7 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
838
954
  let early = consumeFrameTemplate(marker.id) ?? getEarlyFrameContent(marker.id)
839
955
  if (early) {
840
956
  clearPendingFrameTemplateWatch()
841
- await render(early, { initialHydrationTracker, signal, contentStatus: 'resolved' })
957
+ await render(early, { reconciliationTracker, signal, contentStatus: 'resolved' })
842
958
  if (!disposed && !context.lifecycleSignal.aborted && !signal?.aborted) onResolved?.()
843
959
  return
844
960
  }
@@ -872,7 +988,7 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
872
988
  let buffered = consumeFrameTemplate(marker.id)
873
989
  if (buffered) {
874
990
  clearPendingFrameTemplateWatch()
875
- await render(buffered, { initialHydrationTracker, signal, contentStatus: 'resolved' })
991
+ await render(buffered, { reconciliationTracker, signal, contentStatus: 'resolved' })
876
992
  if (!disposed && !context.lifecycleSignal.aborted && !signal?.aborted) onResolved?.()
877
993
  }
878
994
  }
@@ -890,7 +1006,8 @@ export function createFrameRuntime(init: {
890
1006
  moduleLoads: Map<string, Promise<ElementFunction | undefined>>
891
1007
  frameInstances: WeakMap<Comment, Frame>
892
1008
  namedFrames: Map<string, FrameHandle>
893
- reloadForNavigation?: (options?: FrameReloadOptions) => Promise<FrameReloadResult>
1009
+ processClientEntryPreloads?: ProcessClientEntryPreloads
1010
+ reloadForNavigation?: (options?: FrameReloadOptions) => FrameReloadTransition
894
1011
  }): FrameRuntime {
895
1012
  return {
896
1013
  [FRAME_RUNTIME]: true,
@@ -905,47 +1022,66 @@ export function createFrameRuntime(init: {
905
1022
  moduleLoads: init.moduleLoads,
906
1023
  frameInstances: init.frameInstances,
907
1024
  namedFrames: init.namedFrames,
1025
+ processClientEntryPreloads: init.processClientEntryPreloads,
908
1026
  serverFrameReload: undefined,
909
1027
  reloadForNavigation: init.reloadForNavigation,
910
1028
  }
911
1029
  }
912
1030
 
913
- type InitialHydrationTracker = {
1031
+ export type ReconciliationTracker = {
914
1032
  track: () => () => void
1033
+ waitFor: (task: Promise<void>) => void
915
1034
  finalize: () => void
916
1035
  ready: () => Promise<void>
917
1036
  }
918
1037
 
919
- function createInitialHydrationTracker(): InitialHydrationTracker {
1038
+ function createReconciliationTracker(): ReconciliationTracker {
920
1039
  let pending = 0
921
1040
  let finalized = false
1041
+ let failed = false
1042
+ let failure: unknown
922
1043
 
923
1044
  let resolveReady: (() => void) | undefined
924
- let readyPromise = new Promise<void>((resolve) => {
1045
+ let rejectReady: ((error: unknown) => void) | undefined
1046
+ let readyPromise = new Promise<void>((resolve, reject) => {
925
1047
  resolveReady = resolve
1048
+ rejectReady = reject
926
1049
  })
927
1050
 
928
- function maybeResolve() {
929
- if (finalized && pending === 0) {
930
- resolveReady?.()
931
- resolveReady = undefined
1051
+ function maybeSettle() {
1052
+ if (!finalized || pending !== 0) return
1053
+ if (failed) rejectReady?.(failure)
1054
+ else resolveReady?.()
1055
+ resolveReady = undefined
1056
+ rejectReady = undefined
1057
+ }
1058
+
1059
+ function track(): () => void {
1060
+ pending++
1061
+ let completed = false
1062
+ return () => {
1063
+ if (completed) return
1064
+ completed = true
1065
+ pending--
1066
+ maybeSettle()
932
1067
  }
933
1068
  }
934
1069
 
935
1070
  return {
936
- track() {
937
- pending++
938
- let completed = false
939
- return () => {
940
- if (completed) return
941
- completed = true
942
- pending--
943
- maybeResolve()
944
- }
1071
+ track,
1072
+ waitFor(task) {
1073
+ let complete = track()
1074
+ void task.then(complete, (error) => {
1075
+ if (!failed) {
1076
+ failed = true
1077
+ failure = error
1078
+ }
1079
+ complete()
1080
+ })
945
1081
  },
946
1082
  finalize() {
947
1083
  finalized = true
948
- maybeResolve()
1084
+ maybeSettle()
949
1085
  },
950
1086
  ready() {
951
1087
  return readyPromise
@@ -1006,7 +1142,7 @@ function collectOwnedServerStyleTags(nodes: Node[], styles: HTMLStyleElement[]):
1006
1142
  continue
1007
1143
  }
1008
1144
 
1009
- if (node instanceof HTMLStyleElement && node.matches('style[data-rmx]')) {
1145
+ if (node instanceof HTMLStyleElement && node.matches('style[data-rmx-style]')) {
1010
1146
  styles.push(node)
1011
1147
  continue
1012
1148
  }
@@ -1049,7 +1185,7 @@ function copyOwnRmxEntries<T>(target: Record<string, T>, source: Record<string,
1049
1185
  function scheduleHydrationInContainer(
1050
1186
  container: FrameContainer,
1051
1187
  context: FrameContext,
1052
- initialHydrationTracker?: InitialHydrationTracker,
1188
+ reconciliationTracker?: ReconciliationTracker,
1053
1189
  signal?: AbortSignal,
1054
1190
  ): void {
1055
1191
  let hydrationMarkers = findHydrationMarkers(container)
@@ -1061,7 +1197,7 @@ function scheduleHydrationInContainer(
1061
1197
  for (let marker of hydrationMarkers) {
1062
1198
  let entry = hydrationData[marker.id]
1063
1199
  if (!entry) continue
1064
- scheduleHydrationMarker(marker, entry, context, initialHydrationTracker, signal)
1200
+ scheduleHydrationMarker(marker, entry, context, reconciliationTracker, signal)
1065
1201
  }
1066
1202
  }
1067
1203
 
@@ -1069,12 +1205,12 @@ function scheduleHydrationMarker(
1069
1205
  marker: HydrationMarker,
1070
1206
  entry: HydrationData,
1071
1207
  context: FrameContext,
1072
- initialHydrationTracker?: InitialHydrationTracker,
1208
+ reconciliationTracker?: ReconciliationTracker,
1073
1209
  signal?: AbortSignal,
1074
1210
  ): void {
1075
1211
  if (signal?.aborted || context.lifecycleSignal.aborted) return
1076
1212
 
1077
- let done = initialHydrationTracker?.track()
1213
+ let done = reconciliationTracker?.track()
1078
1214
  let key = `${entry.moduleUrl}#${entry.exportName}`
1079
1215
  let identity: ClientEntryIdentity = {
1080
1216
  moduleUrl: entry.moduleUrl,
@@ -1216,30 +1352,41 @@ function hydrateRegion(
1216
1352
 
1217
1353
  context.pendingClientEntries.delete(start)
1218
1354
 
1219
- // The same marker can be discovered by overlapping hydration passes
1220
- // (for example, document root + nested frame root). Reuse the existing
1221
- // virtual root instead of redefining the marker property.
1222
- let owner = getClientEntryBoundaryOwner(start)
1223
- if (owner) {
1355
+ // During a server frame reload, expose the reload signal and reconciliation
1356
+ // tracker to client frames created while this entry renders so blocking
1357
+ // frames keep the reload pending until their content arrives.
1358
+ let renderEntry = (root: Pick<VirtualRoot, 'render'>) => {
1224
1359
  if (!signal) {
1225
- owner.root.render(vElement)
1360
+ root.render(vElement)
1226
1361
  return
1227
1362
  }
1228
1363
 
1229
1364
  let frameRuntime = context.frame.$runtime
1230
1365
  invariant(
1231
1366
  isFrameRuntime(frameRuntime),
1232
- 'Expected frame runtime while rendering a preserved client entry',
1367
+ 'Expected frame runtime while rendering a client entry during a reload',
1233
1368
  )
1234
1369
 
1235
1370
  let previousServerFrameReload = frameRuntime.serverFrameReload
1236
1371
 
1237
- frameRuntime.serverFrameReload = { signal }
1372
+ frameRuntime.serverFrameReload = {
1373
+ signal,
1374
+ reconciliationTracker: context.reconciliationTracker,
1375
+ blockingFrameTracker: context.blockingFrameTracker,
1376
+ }
1238
1377
  try {
1239
- owner.root.render(vElement)
1378
+ root.render(vElement)
1240
1379
  } finally {
1241
1380
  frameRuntime.serverFrameReload = previousServerFrameReload
1242
1381
  }
1382
+ }
1383
+
1384
+ // The same marker can be discovered by overlapping hydration passes
1385
+ // (for example, document root + nested frame root). Reuse the existing
1386
+ // virtual root instead of redefining the marker property.
1387
+ let owner = getClientEntryBoundaryOwner(start)
1388
+ if (owner) {
1389
+ renderEntry(owner.root)
1243
1390
  return
1244
1391
  }
1245
1392
 
@@ -1254,7 +1401,7 @@ function hydrateRegion(
1254
1401
  })
1255
1402
 
1256
1403
  setClientEntryBoundaryOwner(start, identity, root)
1257
- root.render(vElement)
1404
+ renderEntry(root)
1258
1405
  }
1259
1406
 
1260
1407
  async function createSubFrames(
@@ -1300,8 +1447,12 @@ async function createSubFrames(
1300
1447
  moduleLoads: context.moduleLoads,
1301
1448
  frameInstances: context.frameInstances,
1302
1449
  namedFrames: context.namedFrames,
1450
+ processClientEntryPreloads: context.processClientEntryPreloads,
1303
1451
  })
1304
1452
  context.frameInstances.set(node, subFrame)
1453
+ if (frameMarker.status === 'resolved') {
1454
+ tasks.push(subFrame.ready())
1455
+ }
1305
1456
  }
1306
1457
  }
1307
1458