|
|
@@ -3,7 +3,9 @@
|
|
|
* dependencies activate and exposes the mount operation used by the web boot
|
|
|
* kernel after the complete client roster settles.
|
|
|
*/
|
|
|
-import { createRoot } from 'react-dom/client'
|
|
|
+import { createElement, useLayoutEffect, useState, type ReactNode } from 'react'
|
|
|
+import { flushSync } from 'react-dom'
|
|
|
+import { createRoot, hydrateRoot, type Root } from 'react-dom/client'
|
|
|
import type { Context } from '@deepseek-ai/cordis'
|
|
|
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots'
|
|
|
import { createSlotRenderer } from './scoped-slots.tsx'
|
|
|
@@ -42,6 +44,37 @@ declare module '@deepseek-ai/cordis' {
|
|
|
/** Services required before application assembly. */
|
|
|
export const inject = ['slots', 'sessions']
|
|
|
|
|
|
+interface BootSnapshot {
|
|
|
+ className: string
|
|
|
+ html: string
|
|
|
+}
|
|
|
+
|
|
|
+/** Hydrate the kernel-owned loading DOM before replacing it with the application. */
|
|
|
+function BootHandoff(props: { app: () => ReactNode; boot: BootSnapshot }): ReactNode {
|
|
|
+ const [ready, setReady] = useState(false)
|
|
|
+ useLayoutEffect(() => { setReady(true) }, [])
|
|
|
+ if (ready) return props.app()
|
|
|
+ return createElement('div', {
|
|
|
+ className: props.boot.className,
|
|
|
+ 'data-dsh-boot': '',
|
|
|
+ dangerouslySetInnerHTML: { __html: props.boot.html },
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+/** Mount React while preserving the framework-free boot DOM through hydration. */
|
|
|
+function mountApp(container: HTMLElement, app: () => ReactNode): Root {
|
|
|
+ const boot = container.querySelector<HTMLElement>(':scope > [data-dsh-boot]')
|
|
|
+ if (boot !== null) {
|
|
|
+ return hydrateRoot(container, createElement(BootHandoff, {
|
|
|
+ app,
|
|
|
+ boot: { className: boot.className, html: boot.innerHTML },
|
|
|
+ }))
|
|
|
+ }
|
|
|
+ const root = createRoot(container)
|
|
|
+ flushSync(() => { root.render(app()) })
|
|
|
+ return root
|
|
|
+}
|
|
|
+
|
|
|
/**
|
|
|
* Install the slot renderer and provide the application mount face.
|
|
|
* @param ctx - Plugin context.
|
|
|
@@ -50,8 +83,7 @@ export function apply(ctx: Context): void {
|
|
|
ctx.slots.install(createSlotRenderer())
|
|
|
ctx.reflect.provide('uiRenderer', {
|
|
|
mount: (container: HTMLElement): (() => void) => {
|
|
|
- const root = createRoot(container)
|
|
|
- root.render(buildRenderApp({ ctx })())
|
|
|
+ const root = mountApp(container, buildRenderApp({ ctx }))
|
|
|
return () => { root.unmount() }
|
|
|
},
|
|
|
})
|