@@ -54,6 +54,8 @@ const createIframeShellDocument = ({ channelId, parentOrigin, importMap }) => {
5454
5555 const __knightedState = {
5656 entrySpecifier: '',
57+ reactRoot: null,
58+ renderedNode: null,
5759 }
5860
5961 const __knightedRuntimeErrorFingerprints = new Set()
@@ -191,14 +193,20 @@ const createIframeShellDocument = ({ channelId, parentOrigin, importMap }) => {
191193
192194 __knightedApplyVisualConfig(config)
193195
194- let runtimeRoot = document.getElementById('knighted-preview-runtime-root')
195- if (!(runtimeRoot instanceof HTMLElement)) {
196- runtimeRoot = document.createElement('div')
197- runtimeRoot.id = 'knighted-preview-runtime-root'
198- document.body.append(runtimeRoot)
196+ if (
197+ __knightedState.reactRoot &&
198+ typeof __knightedState.reactRoot.unmount === 'function'
199+ ) {
200+ __knightedState.reactRoot.unmount()
201+ __knightedState.reactRoot = null
199202 }
200203
201- runtimeRoot.replaceChildren()
204+ if (__knightedState.renderedNode instanceof Node) {
205+ __knightedState.renderedNode.remove()
206+ __knightedState.renderedNode = null
207+ }
208+
209+ document.querySelectorAll('knighted-preview-root').forEach(node => node.remove())
202210
203211 try {
204212 const entryModule = await import(entrySpecifier)
@@ -220,8 +228,10 @@ const createIframeShellDocument = ({ channelId, parentOrigin, importMap }) => {
220228 }
221229
222230 const host = document.createElement('knighted-preview-root')
223- runtimeRoot .append(host)
231+ document.body .append(host)
224232 const root = createRoot(host)
233+ __knightedState.reactRoot = root
234+ __knightedState.renderedNode = host
225235 root.render(output)
226236 } else {
227237 const { jsx } = await import(runtimeSpecifiers.jsxDom)
@@ -231,7 +241,8 @@ const createIframeShellDocument = ({ channelId, parentOrigin, importMap }) => {
231241 throw new Error('Expected a function or const named App.')
232242 }
233243
234- runtimeRoot.append(output)
244+ document.body.append(output)
245+ __knightedState.renderedNode = output
235246 }
236247
237248 __knightedEmit(__knightedMessageTypes.rendered)
@@ -336,12 +347,48 @@ export const createWorkspaceIframePreviewBridge = ({
336347 let active = true
337348 let ready = false
338349 let resolveReady = ( ) => { }
350+ const readyWaiters = new Set ( )
339351 const readyPromise = new Promise ( resolve => {
340352 resolveReady = resolve
341353 } )
342354
343355 let pendingRender = null
344356
357+ const waitForReady = timeoutMs => {
358+ if ( ready ) {
359+ return Promise . resolve ( )
360+ }
361+
362+ if ( ! active ) {
363+ return Promise . reject ( new Error ( 'Preview iframe bridge is not active.' ) )
364+ }
365+
366+ return new Promise ( ( resolve , reject ) => {
367+ const timer = setTimeout ( ( ) => {
368+ readyWaiters . delete ( onDisposed )
369+ reject ( new Error ( 'Workspace preview iframe did not become ready before timeout.' ) )
370+ } , timeoutMs )
371+
372+ const onReady = ( ) => {
373+ clearTimeout ( timer )
374+ readyWaiters . delete ( onDisposed )
375+ resolve ( )
376+ }
377+
378+ const onDisposed = error => {
379+ clearTimeout ( timer )
380+ reject (
381+ error instanceof Error
382+ ? error
383+ : new Error ( 'Preview iframe bridge was disposed before readiness.' ) ,
384+ )
385+ }
386+
387+ readyPromise . then ( onReady )
388+ readyWaiters . add ( onDisposed )
389+ } )
390+ }
391+
345392 const cleanupPendingRender = ( error = null ) => {
346393 if ( ! pendingRender ) {
347394 return
@@ -381,6 +428,10 @@ export const createWorkspaceIframePreviewBridge = ({
381428 return
382429 }
383430
431+ if ( ! iframe . contentWindow || event . source !== iframe . contentWindow ) {
432+ return
433+ }
434+
384435 const data = event ?. data
385436 if ( ! isPreviewProtocolMessage ( { data, channelId } ) ) {
386437 return
@@ -430,6 +481,15 @@ export const createWorkspaceIframePreviewBridge = ({
430481
431482 active = false
432483 window . removeEventListener ( 'message' , onMessage )
484+ if ( readyWaiters . size > 0 ) {
485+ const disposeError = new Error (
486+ 'Preview iframe bridge was disposed before readiness.' ,
487+ )
488+ for ( const notifyDisposed of readyWaiters ) {
489+ notifyDisposed ( disposeError )
490+ }
491+ readyWaiters . clear ( )
492+ }
433493 if ( pendingRender ) {
434494 cleanupPendingRender (
435495 new Error ( 'Preview iframe bridge disposed before render completed.' ) ,
@@ -457,7 +517,7 @@ export const createWorkspaceIframePreviewBridge = ({
457517 throw new Error ( 'Preview iframe render already in flight.' )
458518 }
459519
460- await readyPromise
520+ await waitForReady ( timeoutMs )
461521
462522 return new Promise ( ( resolve , reject ) => {
463523 const timer = setTimeout ( ( ) => {
0 commit comments