nteract
componentisolated-framesource: ok · hit
Isolated Frame
Secure iframe component for rendering untrusted output content. Uses blob URLs with sandbox restrictions to isolate JavaScript execution. Includes message bridge for parent-child communication and widget comm protocol support.
npx shadcn add "https://nteract-elements.vercel.app/r/isolated-frame.json"free
No selection tags yet — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lightRegistry dependencies
@nteract/widget-store
Files7 · source cached 10/8/2026, 9:02:00 PM
registry/outputs/isolated/index.ts
export {
CommBridgeManager,
createCommBridgeManager,
} from "./comm-bridge-manager";
// Message protocol types
export type {
ClearMessage,
EvalMessage,
EvalResultMessage,
IframeErrorMessage,
// Utilities
IframeMessage,
// Iframe → Parent
IframeToParentMessage,
LinkClickMessage,
MessageType,
// Parent → Iframe
ParentToIframeMessage,
PingMessage,
PongMessage,
ReadyMessage,
RenderCompleteMessage,
RenderMessage,
RenderPayload,
ResizeMessage,
ThemeMessage,
WidgetStateMessage,
WidgetUpdateMessage,
} from "./frame-bridge";
export { isIframeMessage, isMessageType } from "./frame-bridge";
export type { FrameHtmlOptions } from "./frame-html";
// HTML template generator
export { createFrameBlobUrl, generateFrameHtml } from "./frame-html";
// Security testing component
export { IsolationTest } from "./IsolationTest";
export type { IsolatedFrameHandle, IsolatedFrameProps } from "./isolated-frame";
export { IsolatedFrame } from "./isolated-frame";
// Provider and hook for renderer bundle
export {
IsolatedRendererProvider,
useIsolatedRenderer,
} from "./isolated-renderer-context";
registry/outputs/isolated/isolated-frame.tsx
"use client";
import {
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useRef,
useState,
} from "react";
import type {
IframeToParentMessage,
ParentToIframeMessage,
RenderPayload,
} from "./frame-bridge";
import { isIframeMessage } from "./frame-bridge";
import { createFrameBlobUrl } from "./frame-html";
import { useIsolatedRenderer } from "./isolated-renderer-context";
export interface IsolatedFrameProps {
/**
* Unique ID for this frame (used for message routing).
*/
id?: string;
/**
* Initial content to render when the frame is ready.
*/
initialContent?: RenderPayload;
/**
* Whether to use dark mode styling.
*/
darkMode?: boolean;
/**
* Minimum height of the iframe in pixels.
* @default 24
*/
minHeight?: number;
/**
* Maximum height of the iframe in pixels.
* @default 2000
*/
maxHeight?: number;
/**
* Additional CSS classes for the iframe container.
*/
className?: string;
/**
* Callback when the iframe is ready to receive messages.
*/
onReady?: () => void;
/**
* Callback when the iframe content resizes.
*/
onResize?: (height: number) => void;
/**
* Callback when a link is clicked in the iframe.
*/
onLinkClick?: (url: string, newTab: boolean) => void;
/**
* Callback when the user double-clicks in the iframe.
*/
onDoubleClick?: () => void;
/**
* Callback when a widget state update is sent from the iframe.
*/
onWidgetUpdate?: (commId: string, state: Record<string, unknown>) => void;
/**
* Callback when an error occurs in the iframe.
*/
onError?: (error: { message: string; stack?: string }) => void;
/**
* Callback for all messages from the iframe (for debugging or custom handling).
*/
onMessage?: (message: IframeToParentMessage) => void;
}
export interface IsolatedFrameHandle {
/**
* Send a message to the iframe.
*/
send: (message: ParentToIframeMessage) => void;
/**
* Send content to render in the iframe.
*/
render: (payload: RenderPayload) => void;
/**
* Evaluate code in the iframe (for bootstrap/injection).
*/
eval: (code: string) => void;
/**
* Update theme settings in the iframe.
*/
setTheme: (isDark: boolean) => void;
/**
* Clear all content in the iframe.
*/
clear: () => void;
/**
* Search for text within the iframe's rendered content.
* Pass empty string to clear search highlights.
*/
search: (query: string, caseSensitive?: boolean) => void;
/**
* Navigate to a specific search match by index.
*/
searchNavigate: (matchIndex: number) => void;
/**
* Whether the iframe is ready to receive messages.
* True after the React renderer bundle is initialized.
*/
isReady: boolean;
/**
* Whether the iframe bootstrap HTML is loaded.
* True before the React renderer bundle is loaded.
*/
isIframeReady: boolean;
}
/**
* Sandbox attributes for the isolated iframe.
*
* CRITICAL: Do NOT include 'allow-same-origin' - this would give the iframe
* access to the parent's origin and Tauri APIs.
*/
const SANDBOX_ATTRS = [
"allow-scripts", // Required for rendering interactive content
"allow-downloads", // Allow file downloads (e.g., from widgets)
"allow-forms", // Allow form submissions
"allow-pointer-lock", // For interactive visualizations
"allow-popups", // Allow window.open (for links)
"allow-popups-to-escape-sandbox", // Popups should be unrestricted
"allow-modals", // Allow alert/confirm/prompt
].join(" ");
/**
* IsolatedFrame component - Renders untrusted content in a secure iframe.
*
* Uses a blob: URL with sandbox restrictions to ensure the iframe content
* cannot access Tauri APIs or the parent DOM. Communication happens via
* postMessage.
*
* **Requires** `IsolatedRendererProvider` to be present in the component tree.
*
* @example
* ```tsx
* // In your app root or layout:
* <IsolatedRendererProvider basePath="/isolated">
* <App />
* </IsolatedRendererProvider>
*
* // Then use IsolatedFrame anywhere:
* const frameRef = useRef<IsolatedFrameHandle>(null);
*
* <IsolatedFrame
* ref={frameRef}
* darkMode={true}
* onReady={() => {
* frameRef.current?.render({
* mimeType: "text/html",
* data: "<h1>Hello from isolated frame!</h1>"
* });
* }}
* onResize={(height) => console.log("New height:", height)}
* />
* ```
*/
export const IsolatedFrame = forwardRef<
IsolatedFrameHandle,
IsolatedFrameProps
>(function IsolatedFrame(
{
id,
initialContent,
darkMode = true,
minHeight = 24,
maxHeight = 2000,
className = "",
onReady,
onResize,
onLinkClick,
onDoubleClick,
onWidgetUpdate,
onError,
onMessage,
},
ref,
) {
// Get renderer bundle from context (provided by IsolatedRendererProvider)
const {
rendererCode,
rendererCss,
isLoading: providerLoading,
error: providerError,
} = useIsolatedRenderer();
const iframeRef = useRef<HTMLIFrameElement>(null);
const [blobUrl, setBlobUrl] = useState<string | null>(null);
// Track iframe ready (bootstrap HTML loaded)
const [isIframeReady, setIsIframeReady] = useState(false);
// Track renderer ready (React bundle initialized)
const [isReady, setIsReady] = useState(false);
// Use ref to track ready state for send callback (avoids stale closure)
const isReadyRef = useRef(false);
const [height, setHeight] = useState(minHeight);
// Queue messages until iframe is ready
const pendingMessagesRef = useRef<ParentToIframeMessage[]>([]);
// Track if we've started bootstrapping to avoid double-fetch
const bootstrappingRef = useRef(false);
// Track initial darkMode for blob URL (don't recreate blob on theme change)
const initialDarkModeRef = useRef(darkMode);
// Create blob URL on mount (only once, with initial darkMode)
useEffect(() => {
const url = createFrameBlobUrl({ darkMode: initialDarkModeRef.current });
setBlobUrl(url);
return () => {
URL.revokeObjectURL(url);
};
}, []);
// Forward theme changes to iframe (without recreating the blob)
useEffect(() => {
if (isReady && iframeRef.current?.contentWindow) {
iframeRef.current.contentWindow.postMessage(
{ type: "theme", payload: { isDark: darkMode } },
"*",
);
}
}, [darkMode, isReady]);
// Keep ref in sync with state (ref avoids stale closures in callbacks)
useEffect(() => {
isReadyRef.current = isReady;
}, [isReady]);
// Surface provider errors to consumers
useEffect(() => {
if (providerError && !providerLoading) {
onError?.({ message: providerError.message, stack: providerError.stack });
}
}, [providerError, providerLoading, onError]);
// Send a message to the iframe
// Uses ref to check ready state to avoid stale closure issues
const send = useCallback(
(message: ParentToIframeMessage) => {
if (!isReadyRef.current) {
// Queue message until ready
pendingMessagesRef.current.push(message);
return;
}
if (iframeRef.current?.contentWindow) {
iframeRef.current.contentWindow.postMessage(message, "*");
}
},
[], // No deps - uses ref instead of state
);
// Flush pending messages when ready
useEffect(() => {
if (isReady && pendingMessagesRef.current.length > 0) {
const pending = pendingMessagesRef.current;
pendingMessagesRef.current = [];
pending.forEach((msg) => {
if (iframeRef.current?.contentWindow) {
iframeRef.current.contentWindow.postMessage(msg, "*");
}
});
}
}, [isReady]);
// Handle messages from iframe
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
// Verify the message is from our iframe
if (event.source !== iframeRef.current?.contentWindow) {
return;
}
const data = event.data;
if (!isIframeMessage(data)) {
return;
}
// Call generic message handler
onMessage?.(data);
// Handle specific message types
switch (data.type) {
case "ready":
// Iframe bootstrap HTML is loaded
// Renderer injection is handled by a separate useEffect
setIsIframeReady(true);
break;
case "renderer_ready":
// React renderer bundle is initialized
setIsReady(true);
onReady?.();
// Render initial content if provided
if (initialContent) {
iframeRef.current?.contentWindow?.postMessage(
{ type: "render", payload: initialContent },
"*",
);
}
break;
case "resize":
if (data.payload?.height != null) {
const newHeight = Math.max(
minHeight,
Math.min(maxHeight, data.payload.height),
);
setHeight(newHeight);
onResize?.(newHeight);
}
break;
case "link_click":
if (data.payload?.url) {
onLinkClick?.(data.payload.url, data.payload.newTab ?? false);
}
break;
case "dblclick":
onDoubleClick?.();
break;
case "widget_update":
if (data.payload?.commId && data.payload?.state) {
onWidgetUpdate?.(data.payload.commId, data.payload.state);
}
break;
case "error":
if (data.payload) {
onError?.(data.payload);
}
break;
case "eval_result":
// Surface bundle eval failures to help diagnose injection issues
if (data.payload?.success === false) {
console.error(
"[IsolatedFrame] Bundle eval failed:",
data.payload.error,
);
onError?.({ message: `Bundle eval failed: ${data.payload.error}` });
}
break;
}
};
window.addEventListener("message", handleMessage);
return () => window.removeEventListener("message", handleMessage);
}, [
initialContent,
minHeight,
maxHeight,
onReady,
onResize,
onLinkClick,
onDoubleClick,
onWidgetUpdate,
onError,
onMessage,
]);
// Inject renderer when iframe is ready AND bundle props are available
useEffect(() => {
if (
isIframeReady &&
!isReady &&
!bootstrappingRef.current &&
rendererCode &&
rendererCss &&
iframeRef.current?.contentWindow
) {
bootstrappingRef.current = true;
// Inject CSS first (idempotent - checks if already loaded)
const cssCode = `
(function() {
if (window.__ISOLATED_CSS_LOADED__) return;
window.__ISOLATED_CSS_LOADED__ = true;
var style = document.createElement('style');
style.textContent = ${JSON.stringify(rendererCss)};
document.head.appendChild(style);
})();
`;
iframeRef.current.contentWindow.postMessage(
{ type: "eval", payload: { code: cssCode } },
"*",
);
// Then inject JS bundle (idempotent - checks if already loaded)
// Use string concatenation instead of template literal to avoid issues
// with backticks or ${} in the bundled code
const jsWrapper =
"(function() {" +
"if (window.__ISOLATED_RENDERER_LOADED__) return;" +
"window.__ISOLATED_RENDERER_LOADED__ = true;" +
rendererCode +
"})();";
iframeRef.current.contentWindow.postMessage(
{ type: "eval", payload: { code: jsWrapper } },
"*",
);
}
}, [isIframeReady, isReady, rendererCode, rendererCss]);
// Expose imperative API
useImperativeHandle(
ref,
() => ({
send,
render: (payload: RenderPayload) => send({ type: "render", payload }),
eval: (code: string) => send({ type: "eval", payload: { code } }),
setTheme: (isDark: boolean) =>
send({ type: "theme", payload: { isDark } }),
clear: () => send({ type: "clear" }),
search: (query: string, caseSensitive?: boolean) => {
// Search handler is in bootstrap HTML, so send directly when iframe is loaded
// (bypasses the isReady queue which waits for the React renderer)
if (iframeRef.current?.contentWindow) {
iframeRef.current.contentWindow.postMessage(
{ type: "search", payload: { query, caseSensitive } },
"*",
);
}
},
searchNavigate: (matchIndex: number) => {
if (iframeRef.current?.contentWindow) {
iframeRef.current.contentWindow.postMessage(
{ type: "search_navigate", payload: { matchIndex } },
"*",
);
}
},
isReady,
isIframeReady,
}),
[send, isReady, isIframeReady],
);
if (!blobUrl) {
return null;
}
return (
<iframe
ref={iframeRef}
id={id}
src={blobUrl}
sandbox={SANDBOX_ATTRS}
className={className}
data-slot="isolated-frame"
style={{
width: "100%",
height: `${height}px`,
border: "none",
display: "block",
}}
title="Isolated output frame"
/>
);
});
registry/outputs/isolated/frame-bridge.ts
/**
* Message protocol types for parent ↔ iframe communication.
*
* This module defines the contract between the parent window and isolated output frames.
* All communication happens via postMessage with structured message types.
*/
// --- Message Types: Parent → Iframe ---
/**
* Bootstrap the iframe with JavaScript code.
* Used to inject the ESM renderer bundle into the iframe.
*/
export interface EvalMessage {
type: "eval";
payload: {
/** JavaScript code to evaluate in the iframe context */
code: string;
};
}
/**
* Render output content in the iframe.
*/
export interface RenderMessage {
type: "render";
payload: RenderPayload;
}
export interface RenderPayload {
/** MIME type of the content (e.g., "text/html", "text/markdown") */
mimeType: string;
/** The content data (format depends on MIME type) */
data: unknown;
/** Optional metadata for the output */
metadata?: Record<string, unknown>;
/** Cell ID this output belongs to (for routing) */
cellId?: string;
/** Output index within the cell */
outputIndex?: number;
/** If true, append to existing outputs instead of replacing */
append?: boolean;
/** If true, replace all existing outputs with this single output */
replace?: boolean;
}
/**
* Update widget state in the iframe.
*/
export interface WidgetStateMessage {
type: "widget_state";
payload: {
/** Comm ID of the widget */
commId: string;
/** Updated state to merge */
state: Record<string, unknown>;
/** Optional buffers (base64 encoded) */
buffers?: string[];
};
}
/**
* Sync theme with the iframe.
*/
export interface ThemeMessage {
type: "theme";
payload: {
/** Whether dark mode is active */
isDark: boolean;
/** Optional CSS variables to inject */
cssVariables?: Record<string, string>;
};
}
/**
* Ping the iframe (for health checks and latency measurement).
*/
export interface PingMessage {
type: "ping";
payload?: {
sentAt: number;
};
}
/**
* Clear all rendered content in the iframe.
*/
export interface ClearMessage {
type: "clear";
}
// --- Widget Comm Protocol: Parent → Iframe ---
/**
* Forward a comm_open message to the iframe.
* Sent when a widget model is created by the kernel.
*/
export interface CommOpenMessage {
type: "comm_open";
payload: {
/** Comm ID of the widget */
commId: string;
/** Target name (e.g., "jupyter.widget") */
targetName: string;
/** Initial widget state */
state: Record<string, unknown>;
/** Buffer paths for binary data reconstruction */
bufferPaths?: string[][];
/** Binary buffers (transferred via structured clone) */
buffers?: ArrayBuffer[];
};
}
/**
* Forward a comm_msg to the iframe.
* Sent for state updates and custom messages from kernel.
*/
export interface CommMsgMessage {
type: "comm_msg";
payload: {
/** Comm ID of the widget */
commId: string;
/** Message method: "update" or "custom" */
method: "update" | "custom";
/** State patch (for update) or custom content (for custom) */
data: Record<string, unknown>;
/** Buffer paths for binary data reconstruction */
bufferPaths?: string[][];
/** Binary buffers (transferred via structured clone) */
buffers?: ArrayBuffer[];
};
}
/**
* Forward a comm_close message to the iframe.
* Sent when a widget is destroyed by the kernel.
*/
export interface CommCloseMessage {
type: "comm_close";
payload: {
/** Comm ID of the widget to close */
commId: string;
};
}
/**
* Sync all existing widget models to the iframe.
* Sent on iframe ready to bootstrap existing widgets.
*/
export interface CommSyncMessage {
type: "comm_sync";
payload: {
/** Array of existing models to sync */
models: Array<{
commId: string;
targetName: string;
state: Record<string, unknown>;
buffers?: ArrayBuffer[];
}>;
};
}
/**
* Signal that the parent's comm bridge is ready.
* Iframe should respond with widget_ready to trigger comm_sync.
*/
export interface BridgeReadyMessage {
type: "bridge_ready";
}
// --- Global Find: Parent → Iframe ---
/**
* Search for text within the iframe's rendered content.
* The iframe should highlight all matches and report the count.
*/
export interface SearchMessage {
type: "search";
payload: {
/** The search query string (empty string clears search) */
query: string;
/** Whether the search should be case-sensitive */
caseSensitive?: boolean;
};
}
/**
* Navigate to a specific match in the iframe's search results.
*/
export interface SearchNavigateMessage {
type: "search_navigate";
payload: {
/** The index of the match to navigate to (0-based) */
matchIndex: number;
};
}
/**
* All message types that can be sent from parent to iframe.
*/
export type ParentToIframeMessage =
| EvalMessage
| RenderMessage
| WidgetStateMessage
| ThemeMessage
| PingMessage
| ClearMessage
| CommOpenMessage
| CommMsgMessage
| CommCloseMessage
| CommSyncMessage
| BridgeReadyMessage
| SearchMessage
| SearchNavigateMessage;
// --- Message Types: Iframe → Parent ---
/**
* Iframe has finished loading and is ready to receive messages.
*/
export interface ReadyMessage {
type: "ready";
}
/**
* Response to a ping message.
*/
export interface PongMessage {
type: "pong";
payload: {
receivedAt: number;
/** Echo back the payload from the ping */
echo?: unknown;
};
}
/**
* Result of evaluating code in the iframe.
*/
export interface EvalResultMessage {
type: "eval_result";
payload: {
success: boolean;
result?: string;
error?: string;
};
}
/**
* Iframe content has finished rendering.
*/
export interface RenderCompleteMessage {
type: "render_complete";
payload?: {
/** Height of the rendered content */
height?: number;
};
}
/**
* Iframe content size has changed.
*/
export interface ResizeMessage {
type: "resize";
payload: {
/** New height of the content */
height: number;
/** New width of the content (optional) */
width?: number;
};
}
/**
* User clicked a link in the iframe.
*/
export interface LinkClickMessage {
type: "link_click";
payload: {
/** The URL that was clicked */
url: string;
/** Whether it was a ctrl/cmd click */
newTab: boolean;
};
}
/**
* User double-clicked in the iframe.
*/
export interface DoubleClickMessage {
type: "dblclick";
}
/**
* Widget state was updated in the iframe (needs to sync to kernel).
*/
export interface WidgetUpdateMessage {
type: "widget_update";
payload: {
/** Comm ID of the widget */
commId: string;
/** Updated state */
state: Record<string, unknown>;
/** Optional buffers (base64 encoded) */
buffers?: string[];
};
}
/**
* An error occurred in the iframe.
*/
export interface IframeErrorMessage {
type: "error";
payload: {
message: string;
stack?: string;
};
}
/**
* The React renderer bundle has been loaded and initialized.
* This is sent after the bundle is eval'd and React is mounted.
*/
export interface RendererReadyMessage {
type: "renderer_ready";
}
// --- Widget Comm Protocol: Iframe → Parent ---
/**
* Iframe widget system is ready to receive comm messages.
* Parent should send comm_sync with existing models after this.
*/
export interface WidgetReadyMessage {
type: "widget_ready";
}
/**
* Widget initiated a state update or custom message.
* Parent should forward to kernel and update its store.
*/
export interface WidgetCommMsgMessage {
type: "widget_comm_msg";
payload: {
/** Comm ID of the widget */
commId: string;
/** Message method: "update" or "custom" */
method: "update" | "custom";
/** State patch or custom content */
data: Record<string, unknown>;
/** Buffer paths */
bufferPaths?: string[][];
/** Binary buffers */
buffers?: ArrayBuffer[];
};
}
/**
* Widget initiated comm close.
* Parent should forward to kernel and clean up.
*/
export interface WidgetCommCloseMessage {
type: "widget_comm_close";
payload: {
/** Comm ID of the widget to close */
commId: string;
};
}
// --- Global Find: Iframe → Parent ---
/**
* Report search results from the iframe.
* Sent after processing a search message.
*/
export interface SearchResultsMessage {
type: "search_results";
payload: {
/** Number of matches found */
count: number;
};
}
/**
* All message types that can be sent from iframe to parent.
*/
export type IframeToParentMessage =
| ReadyMessage
| PongMessage
| EvalResultMessage
| RenderCompleteMessage
| ResizeMessage
| LinkClickMessage
| DoubleClickMessage
| WidgetUpdateMessage
| IframeErrorMessage
| RendererReadyMessage
| WidgetReadyMessage
| WidgetCommMsgMessage
| WidgetCommCloseMessage
| SearchResultsMessage;
// --- Utility Types ---
/**
* All message types (for generic handling).
*/
export type IframeMessage = ParentToIframeMessage | IframeToParentMessage;
/**
* Extract the message type string.
*/
export type MessageType = IframeMessage["type"];
/**
* Type guard to check if a message is from the iframe.
*/
export function isIframeMessage(data: unknown): data is IframeToParentMessage {
if (typeof data !== "object" || data === null) return false;
const msg = data as { type?: unknown };
return (
typeof msg.type === "string" &&
[
"ready",
"pong",
"eval_result",
"render_complete",
"resize",
"link_click",
"dblclick",
"widget_update",
"error",
"renderer_ready",
"widget_ready",
"widget_comm_msg",
"widget_comm_close",
"search_results",
].includes(msg.type)
);
}
/**
* Type guard for specific message types.
*/
export function isMessageType<T extends IframeMessage["type"]>(
data: unknown,
type: T,
): data is Extract<IframeMessage, { type: T }> {
if (typeof data !== "object" || data === null) return false;
return (data as { type?: unknown }).type === type;
}
registry/outputs/isolated/frame-html.ts
/**
* HTML template generator for isolated output frames.
*
* Creates the minimal HTML document that runs inside the blob URL iframe.
* This document handles the message protocol and provides a render target
* for output content.
*
* Security: This code runs in an isolated origin (blob:) with sandbox
* restrictions, so it cannot access Tauri APIs or the parent DOM.
*/
export interface FrameHtmlOptions {
/**
* Whether to include dark mode styles by default.
*/
darkMode?: boolean;
/**
* Additional CSS to inject into the frame.
*/
additionalCss?: string;
/**
* Additional JavaScript to inject (runs after bootstrap).
*/
additionalScript?: string;
}
/**
* Generate the HTML template for an isolated output frame.
*
* The generated HTML includes:
* - Basic styling for outputs (respects light/dark mode)
* - Message handler for parent communication
* - ResizeObserver for auto-sizing
* - Ready notification on load
*
* @param options - Configuration options for the frame
* @returns HTML string to be used with a blob URL
*/
export function generateFrameHtml(options: FrameHtmlOptions = {}): string {
const {
darkMode = true,
additionalCss = "",
additionalScript = "",
} = options;
return `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="default-src 'self' blob: data:; script-src 'unsafe-inline' 'unsafe-eval' blob: https:; style-src 'unsafe-inline'; img-src * data: blob:; font-src * data:; connect-src *;">
<style>
:root {
--bg-primary: ${darkMode ? "#0a0a0a" : "#ffffff"};
--bg-secondary: ${darkMode ? "#1a1a1a" : "#f5f5f5"};
--text-primary: ${darkMode ? "#e0e0e0" : "#1a1a1a"};
--text-secondary: ${darkMode ? "#a0a0a0" : "#666666"};
--border-color: ${darkMode ? "#333333" : "#e0e0e0"};
--accent-color: #3b82f6;
--error-color: #ef4444;
--success-color: #22c55e;
}
* {
box-sizing: border-box;
}
html, body {
margin: 0;
padding: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.5;
background: transparent;
color: var(--text-primary);
}
/* Output container */
#root {
min-height: 1px;
}
/* Reset common elements - 0.875rem matches Tailwind's text-sm */
pre, code {
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
font-size: 0.875rem;
}
pre {
margin: 0;
padding: 8px;
background: var(--bg-secondary);
border-radius: 4px;
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
}
/* Table styling for pandas DataFrames - 0.875rem matches Tailwind's text-sm */
table {
border-collapse: collapse;
margin: 8px 0;
font-size: 0.875rem;
}
th, td {
border: 1px solid var(--border-color);
padding: 4px 8px;
text-align: left;
}
th {
background: var(--bg-secondary);
font-weight: 600;
}
/* Image outputs */
img {
max-width: 100%;
height: auto;
}
/* Links */
a {
color: var(--accent-color);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* Error styling */
.error {
color: var(--error-color);
}
.error pre {
background: ${darkMode ? "#1a1010" : "#fef2f2"};
color: var(--error-color);
}
${additionalCss}
</style>
</head>
<body>
<div id="root"></div>
<script>
(function() {
'use strict';
// --- State ---
let isReady = false;
const root = document.getElementById('root');
// --- Message Handler ---
// Note: When the React renderer bundle is loaded, it sets window.__REACT_RENDERER_ACTIVE__
// and the inline handlers should defer to React for render/theme/clear messages.
window.addEventListener('message', function(event) {
// Only accept messages from our parent window
if (event.source !== window.parent) {
return;
}
const { type, payload } = event.data || {};
try {
switch (type) {
case 'ping':
handlePing(payload);
break;
case 'eval':
handleEval(payload);
break;
case 'render':
// Skip inline rendering if React renderer is active
if (window.__REACT_RENDERER_ACTIVE__) return;
handleRender(payload);
break;
case 'theme':
// Skip inline theme handling if React renderer is active
if (window.__REACT_RENDERER_ACTIVE__) return;
handleTheme(payload);
break;
case 'clear':
// Skip inline clear if React renderer is active
if (window.__REACT_RENDERER_ACTIVE__) return;
handleClear();
break;
case 'widget_state':
handleWidgetState(payload);
break;
case 'search':
handleSearch(payload);
break;
case 'search_navigate':
handleSearchNavigate(payload);
break;
// Comm bridge messages - handled by React widget system, ignore here
case 'bridge_ready':
case 'comm_open':
case 'comm_msg':
case 'comm_close':
case 'comm_sync':
// These are handled by widget-bridge-client.ts
break;
default:
console.warn('[frame] Unknown message type:', type);
}
} catch (err) {
sendError(err);
}
});
// --- Message Handlers ---
function handlePing(payload) {
send('pong', {
receivedAt: Date.now(),
echo: payload
});
}
function handleEval(payload) {
const { code } = payload || {};
if (!code) {
send('eval_result', { success: false, error: 'No code provided' });
return;
}
// Store the current message for access during eval
window.currentMessage = event;
try {
const result = eval.call(null, code);
send('eval_result', { success: true, result: String(result ?? 'undefined') });
} catch (err) {
send('eval_result', { success: false, error: err.message });
} finally {
delete window.currentMessage;
}
}
function handleRender(payload) {
const { mimeType, data, metadata, append } = payload || {};
// Create output container
const output = document.createElement('div');
output.className = 'output-item';
output.style.marginBottom = '8px';
if (mimeType === 'text/html') {
// Use createContextualFragment for proper script execution
const range = document.createRange();
const fragment = range.createContextualFragment(String(data));
output.appendChild(fragment);
} else if (mimeType === 'text/plain') {
const pre = document.createElement('pre');
// Handle ANSI escape codes for colored output
pre.innerHTML = parseAnsi(String(data));
output.appendChild(pre);
} else if (mimeType === 'image/svg+xml') {
// SVG: render inline
const container = document.createElement('div');
container.innerHTML = String(data);
const svg = container.querySelector('svg');
if (svg) {
svg.style.maxWidth = '100%';
svg.style.height = 'auto';
output.appendChild(svg);
} else {
output.appendChild(container);
}
} else if (mimeType && mimeType.startsWith('image/')) {
const img = document.createElement('img');
const imgData = String(data);
// Check if it's base64 or a URL
if (imgData.startsWith('data:') || imgData.startsWith('http')) {
img.src = imgData;
} else {
img.src = 'data:' + mimeType + ';base64,' + imgData;
}
if (metadata?.width) img.width = metadata.width;
if (metadata?.height) img.height = metadata.height;
output.appendChild(img);
} else if (mimeType === 'application/json') {
// JSON: render as formatted, collapsible tree
const pre = document.createElement('pre');
try {
const parsed = typeof data === 'string' ? JSON.parse(data) : data;
pre.textContent = JSON.stringify(parsed, null, 2);
} catch (e) {
pre.textContent = String(data);
}
output.appendChild(pre);
} else {
// Fallback: render as text
const pre = document.createElement('pre');
pre.textContent = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
output.appendChild(pre);
}
// Append or replace
if (append) {
root.appendChild(output);
} else {
root.innerHTML = '';
root.appendChild(output);
}
// Notify completion
requestAnimationFrame(function() {
send('render_complete', { height: document.body.scrollHeight });
});
}
// Basic ANSI escape code parser
function parseAnsi(text) {
// Simple ANSI color mapping
const colors = {
'30': '#000', '31': '#e74c3c', '32': '#2ecc71', '33': '#f1c40f',
'34': '#3498db', '35': '#9b59b6', '36': '#1abc9c', '37': '#ecf0f1',
'90': '#7f8c8d', '91': '#e74c3c', '92': '#2ecc71', '93': '#f1c40f',
'94': '#3498db', '95': '#9b59b6', '96': '#1abc9c', '97': '#fff'
};
// Escape HTML
let result = text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>');
// Parse ANSI codes
result = result.replace(/\\x1b\\[(\\d+(?:;\\d+)*)m/g, function(match, codes) {
const codeList = codes.split(';');
let style = '';
for (const code of codeList) {
if (code === '0') return '</span>';
if (code === '1') style += 'font-weight:bold;';
if (code === '3') style += 'font-style:italic;';
if (code === '4') style += 'text-decoration:underline;';
if (colors[code]) style += 'color:' + colors[code] + ';';
}
return style ? '<span style="' + style + '">' : '';
});
// Also handle \\e[ format
result = result.replace(/\\e\\[(\\d+(?:;\\d+)*)m/g, function(match, codes) {
const codeList = codes.split(';');
let style = '';
for (const code of codeList) {
if (code === '0') return '</span>';
if (code === '1') style += 'font-weight:bold;';
if (colors[code]) style += 'color:' + colors[code] + ';';
}
return style ? '<span style="' + style + '">' : '';
});
return result;
}
function handleTheme(payload) {
const { isDark, cssVariables } = payload || {};
const rootEl = document.documentElement;
if (isDark !== undefined) {
rootEl.style.setProperty('--bg-primary', isDark ? '#0a0a0a' : '#ffffff');
rootEl.style.setProperty('--bg-secondary', isDark ? '#1a1a1a' : '#f5f5f5');
rootEl.style.setProperty('--text-primary', isDark ? '#e0e0e0' : '#1a1a1a');
rootEl.style.setProperty('--text-secondary', isDark ? '#a0a0a0' : '#666666');
rootEl.style.setProperty('--border-color', isDark ? '#333333' : '#e0e0e0');
}
if (cssVariables) {
Object.entries(cssVariables).forEach(function([key, value]) {
rootEl.style.setProperty(key, value);
});
}
}
function handleClear() {
root.innerHTML = '';
send('render_complete', { height: document.body.scrollHeight });
}
function handleWidgetState(payload) {
// Widget state updates are handled by the injected renderer bundle
// This is a placeholder that fires a custom event
window.dispatchEvent(new CustomEvent('widget_state', { detail: payload }));
}
// --- Search ---
var searchMarks = [];
var currentSearchIndex = -1;
function handleSearch(payload) {
var query = (payload && payload.query) || '';
var caseSensitive = payload && payload.caseSensitive;
clearSearchMarks();
if (!query) {
send('search_results', { count: 0 });
return;
}
var marks = [];
var walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null);
var node;
var compareQuery = caseSensitive ? query : query.toLowerCase();
// Collect all text nodes and their match positions
var matches = [];
while ((node = walker.nextNode())) {
var text = node.nodeValue || '';
var compareText = caseSensitive ? text : text.toLowerCase();
var pos = 0;
while ((pos = compareText.indexOf(compareQuery, pos)) !== -1) {
matches.push({ node: node, offset: pos, length: query.length });
pos += query.length;
}
}
// Highlight matches in reverse order to preserve offsets
for (var i = matches.length - 1; i >= 0; i--) {
var m = matches[i];
try {
var range = document.createRange();
range.setStart(m.node, m.offset);
range.setEnd(m.node, m.offset + m.length);
var mark = document.createElement('mark');
mark.className = 'global-find-match';
mark.style.cssText = 'background: #fbbf24; color: #000; border-radius: 2px; padding: 0;';
range.surroundContents(mark);
marks.unshift(mark);
} catch (e) {
// surroundContents can fail if range crosses element boundaries
}
}
searchMarks = marks;
currentSearchIndex = -1;
send('search_results', { count: marks.length });
}
function handleSearchNavigate(payload) {
var matchIndex = (payload && payload.matchIndex) || 0;
if (searchMarks.length === 0) return;
// Clear previous active highlight
if (currentSearchIndex >= 0 && currentSearchIndex < searchMarks.length) {
searchMarks[currentSearchIndex].style.cssText = 'background: #fbbf24; color: #000; border-radius: 2px; padding: 0;';
}
currentSearchIndex = matchIndex;
if (currentSearchIndex >= 0 && currentSearchIndex < searchMarks.length) {
var active = searchMarks[currentSearchIndex];
active.style.cssText = 'background: #f97316; color: #000; border-radius: 2px; padding: 0;';
active.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}
}
function clearSearchMarks() {
for (var i = 0; i < searchMarks.length; i++) {
var mark = searchMarks[i];
var parent = mark.parentNode;
if (parent) {
while (mark.firstChild) {
parent.insertBefore(mark.firstChild, mark);
}
parent.removeChild(mark);
parent.normalize();
}
}
searchMarks = [];
currentSearchIndex = -1;
}
// --- Utilities ---
function send(type, payload) {
window.parent.postMessage({ type: type, payload: payload }, '*');
}
function sendError(err) {
send('error', {
message: err.message || String(err),
stack: err.stack
});
}
// --- Resize Observer ---
const resizeObserver = new ResizeObserver(function(entries) {
const height = document.body.scrollHeight;
send('resize', { height: height });
});
resizeObserver.observe(document.body);
// --- Link Click Interception ---
document.addEventListener('click', function(e) {
const link = e.target.closest('a');
if (link && link.href) {
e.preventDefault();
send('link_click', {
url: link.href,
newTab: e.metaKey || e.ctrlKey
});
}
});
// --- Double Click Forwarding ---
document.addEventListener('dblclick', function(e) {
// Don't forward double-clicks on links (user is selecting text)
const link = e.target.closest('a');
if (!link) {
send('dblclick', null);
}
});
// --- Error Handler ---
window.addEventListener('error', function(e) {
sendError(e.error || new Error(e.message));
});
window.addEventListener('unhandledrejection', function(e) {
sendError(e.reason || new Error('Unhandled promise rejection'));
});
// --- Additional Script ---
${additionalScript}
// --- Ready Signal ---
isReady = true;
send('ready', null);
})();
</script>
</body>
</html>`;
}
/**
* Create a blob URL from the frame HTML.
*
* @param options - Configuration options for the frame
* @returns A blob: URL that can be used as iframe src
*/
export function createFrameBlobUrl(options?: FrameHtmlOptions): string {
const html = generateFrameHtml(options);
const blob = new Blob([html], { type: "text/html" });
return URL.createObjectURL(blob);
}
registry/outputs/isolated/comm-bridge-manager.ts
/**
* Comm Bridge Manager - Parent Side
*
* This module manages the communication bridge between the parent window's
* widget system and an isolated iframe. It:
* - Buffers comm messages until iframe sends `widget_ready`
* - Syncs all existing widget models to iframe on ready
* - Forwards comm messages from kernel to iframe
* - Handles widget messages from iframe and updates parent store + kernel
*
* Security: The iframe cannot access Tauri APIs directly. All widget
* communication must go through this controlled postMessage bridge.
*/
import type { WidgetStore } from "@/registry/widgets/widget-store";
import type {
CommCloseMessage,
CommMsgMessage,
CommOpenMessage,
CommSyncMessage,
IframeToParentMessage,
} from "./frame-bridge";
import type { IsolatedFrameHandle } from "./isolated-frame";
// Type for sending messages to kernel
type SendUpdate = (
commId: string,
state: Record<string, unknown>,
buffers?: ArrayBuffer[],
) => void;
type SendCustom = (
commId: string,
content: Record<string, unknown>,
buffers?: ArrayBuffer[],
) => void;
type CloseComm = (commId: string) => void;
interface CommBridgeManagerOptions {
/** The isolated frame handle for sending messages */
frame: IsolatedFrameHandle;
/** The parent widget store */
store: WidgetStore;
/** Function to send state updates to kernel */
sendUpdate: SendUpdate;
/** Function to send custom messages to kernel */
sendCustom: SendCustom;
/** Function to close a comm with kernel */
closeComm: CloseComm;
}
/**
* Comm Bridge Manager for proxying widget communication to an isolated iframe.
*
* Usage:
* 1. Create manager when IsolatedFrame is mounted
* 2. Subscribe to widget store changes to forward to iframe
* 3. Handle iframe messages via onMessage callback
* 4. Dispose when iframe is unmounted
*/
export class CommBridgeManager {
private frame: IsolatedFrameHandle;
private store: WidgetStore;
private sendUpdateToKernel: SendUpdate;
private sendCustomToKernel: SendCustom;
private closeCommWithKernel: CloseComm;
private isWidgetReady = false;
private messageBuffer: Array<
CommOpenMessage | CommMsgMessage | CommCloseMessage
> = [];
private storeUnsubscribe: (() => void) | null = null;
// Track which models have been sent to avoid duplicate sends
private sentModels = new Set<string>();
// Track previous state for each model to detect kernel updates
private previousState = new Map<string, Record<string, unknown>>();
// Flag to prevent echoing iframe updates back to iframe
private isProcessingIframeUpdate = false;
// Track custom message subscriptions for each model
private customMessageUnsubscribers = new Map<string, () => void>();
constructor(options: CommBridgeManagerOptions) {
this.frame = options.frame;
this.store = options.store;
this.sendUpdateToKernel = options.sendUpdate;
this.sendCustomToKernel = options.sendCustom;
this.closeCommWithKernel = options.closeComm;
// Subscribe to store changes to forward to iframe
this.storeUnsubscribe = this.store.subscribe(() => {
if (!this.isWidgetReady) return;
// Skip if this change came from iframe (avoid echo)
if (this.isProcessingIframeUpdate) return;
this.syncModels();
});
// Signal to iframe that parent bridge is ready
// Iframe will respond with widget_ready to trigger comm_sync
this.frame.send({ type: "bridge_ready" });
}
/**
* Handle a message from the iframe.
* Call this from the IsolatedFrame's onMessage callback.
*/
handleIframeMessage(message: IframeToParentMessage): void {
switch (message.type) {
case "widget_ready":
this.handleWidgetReady();
break;
case "widget_comm_msg":
this.handleWidgetCommMsg(message.payload);
break;
case "widget_comm_close":
this.handleWidgetCommClose(message.payload);
break;
}
}
/**
* Forward a comm_open to the iframe.
* Called when a widget model is created by the kernel.
*/
sendCommOpen(
commId: string,
targetName: string,
state: Record<string, unknown>,
buffers?: ArrayBuffer[],
): void {
const msg: CommOpenMessage = {
type: "comm_open",
payload: {
commId,
targetName,
state,
buffers,
},
};
if (this.isWidgetReady) {
this.frame.send(msg);
this.sentModels.add(commId);
} else {
this.messageBuffer.push(msg);
}
}
/**
* Forward a comm_msg (state update or custom message) to the iframe.
* Called when the kernel sends a state update or custom message.
*/
sendCommMsg(
commId: string,
method: "update" | "custom",
data: Record<string, unknown>,
buffers?: ArrayBuffer[],
): void {
const msg: CommMsgMessage = {
type: "comm_msg",
payload: {
commId,
method,
data,
buffers,
},
};
if (this.isWidgetReady) {
this.frame.send(msg);
} else {
this.messageBuffer.push(msg);
}
}
/**
* Forward a comm_close to the iframe.
* Called when the kernel closes a widget.
*/
sendCommClose(commId: string): void {
const msg: CommCloseMessage = {
type: "comm_close",
payload: { commId },
};
if (this.isWidgetReady) {
this.frame.send(msg);
this.sentModels.delete(commId);
} else {
this.messageBuffer.push(msg);
}
}
/**
* Clean up subscriptions and state.
*/
dispose(): void {
if (this.storeUnsubscribe) {
this.storeUnsubscribe();
this.storeUnsubscribe = null;
}
// Unsubscribe from all custom message subscriptions
for (const unsubscribe of this.customMessageUnsubscribers.values()) {
unsubscribe();
}
this.customMessageUnsubscribers.clear();
this.messageBuffer = [];
this.sentModels.clear();
this.previousState.clear();
this.isWidgetReady = false;
}
// --- Private Methods ---
private handleWidgetReady(): void {
this.isWidgetReady = true;
// Send comm_sync with all existing models
const models = this.store.getSnapshot();
const modelArray: CommSyncMessage["payload"]["models"] = [];
for (const [commId, model] of models) {
modelArray.push({
commId,
targetName: model.modelModule || "jupyter.widget",
state: model.state,
buffers: model.buffers,
});
this.sentModels.add(commId);
// Store initial state for change detection
this.previousState.set(commId, this.cloneStateSnapshot(model.state));
// Subscribe to custom messages for this model
this.subscribeToModelCustomMessages(commId);
}
if (modelArray.length > 0) {
const syncMsg: CommSyncMessage = {
type: "comm_sync",
payload: { models: modelArray },
};
try {
this.frame.send(syncMsg);
} catch (e) {
console.error("[CommBridge] Error sending comm_sync:", e);
}
}
// Flush buffered messages
for (const msg of this.messageBuffer) {
this.frame.send(msg);
if (msg.type === "comm_open") {
this.sentModels.add(msg.payload.commId);
} else if (msg.type === "comm_close") {
this.sentModels.delete(msg.payload.commId);
}
}
this.messageBuffer = [];
}
private handleWidgetCommMsg(payload: {
commId: string;
method: "update" | "custom";
data: Record<string, unknown>;
bufferPaths?: string[][];
buffers?: ArrayBuffer[];
}): void {
const { commId, method, data, buffers } = payload;
if (method === "update") {
// Set flag to prevent echoing this update back to iframe
this.isProcessingIframeUpdate = true;
try {
// Update parent store first (so UI stays in sync)
this.store.updateModel(commId, data, buffers);
// Update our tracked state
const current = this.previousState.get(commId) ?? {};
this.previousState.set(
commId,
this.cloneStateSnapshot({ ...current, ...data }),
);
// Then forward to kernel
this.sendUpdateToKernel(commId, data, buffers);
} finally {
this.isProcessingIframeUpdate = false;
}
} else if (method === "custom") {
// Custom messages go directly to kernel (no store update)
this.sendCustomToKernel(commId, data, buffers);
}
}
private handleWidgetCommClose(payload: { commId: string }): void {
const { commId } = payload;
// Update parent store
this.store.deleteModel(commId);
// Forward to kernel
this.closeCommWithKernel(commId);
// Clean up tracking
this.sentModels.delete(commId);
}
/**
* Sync models with iframe: new models, deleted models, and state changes.
* Called when store changes after widget_ready.
*/
private syncModels(): void {
const models = this.store.getSnapshot();
for (const [commId, model] of models) {
if (!this.sentModels.has(commId)) {
// New model - send comm_open
this.sendCommOpen(
commId,
model.modelModule || "jupyter.widget",
model.state,
model.buffers,
);
// Store initial state for change detection
this.previousState.set(commId, this.cloneStateSnapshot(model.state));
// Subscribe to custom messages for this model
this.subscribeToModelCustomMessages(commId);
} else {
// Existing model - check for state changes
const previous = this.previousState.get(commId);
if (previous) {
const changedKeys = this.getChangedKeys(previous, model.state);
if (changedKeys.length > 0) {
// Build delta with only changed keys
const delta: Record<string, unknown> = {};
for (const key of changedKeys) {
delta[key] = model.state[key];
}
// Forward state update to iframe
this.sendCommMsg(commId, "update", delta, model.buffers);
// Update tracked state
this.previousState.set(
commId,
this.cloneStateSnapshot(model.state),
);
}
}
}
}
// Check for deleted models
for (const commId of this.sentModels) {
if (!models.has(commId)) {
this.sendCommClose(commId);
this.previousState.delete(commId);
// Unsubscribe from custom messages
this.unsubscribeFromModelCustomMessages(commId);
}
}
}
/**
* Subscribe to custom messages for a model and forward them to iframe.
* This is critical for anywidgets like quak that use custom messages for data.
*/
private subscribeToModelCustomMessages(commId: string): void {
// Don't double-subscribe
if (this.customMessageUnsubscribers.has(commId)) return;
const unsubscribe = this.store.subscribeToCustomMessage(
commId,
(content: Record<string, unknown>, buffers?: DataView[]) => {
// Convert DataView[] to ArrayBuffer[] for postMessage
const arrayBuffers = buffers?.map(
(dv: DataView) => dv.buffer as ArrayBuffer,
);
// Forward custom message to iframe
this.sendCommMsg(commId, "custom", content, arrayBuffers);
},
);
this.customMessageUnsubscribers.set(commId, unsubscribe);
}
/**
* Unsubscribe from custom messages for a model.
*/
private unsubscribeFromModelCustomMessages(commId: string): void {
const unsubscribe = this.customMessageUnsubscribers.get(commId);
if (unsubscribe) {
unsubscribe();
this.customMessageUnsubscribers.delete(commId);
}
}
/**
* Get keys that have changed between two state objects.
* Uses shallow comparison for performance.
*/
private getChangedKeys(
previous: Record<string, unknown>,
current: Record<string, unknown>,
): string[] {
const changed: string[] = [];
const allKeys = new Set([
...Object.keys(previous),
...Object.keys(current),
]);
for (const key of allKeys) {
if (this.valuesAreDifferent(previous[key], current[key])) {
changed.push(key);
}
}
return changed;
}
/**
* Create a deep snapshot of model state so future in-place mutations are detectable.
*/
private cloneStateSnapshot(
state: Record<string, unknown>,
): Record<string, unknown> {
try {
return structuredClone(state);
} catch {
return { ...state };
}
}
/**
* Compare two state values.
* For object/array values, use JSON content comparison so deep snapshots
* can detect in-place mutations from live state objects.
*/
private valuesAreDifferent(previous: unknown, current: unknown): boolean {
if (
typeof previous !== "object" ||
previous === null ||
typeof current !== "object" ||
current === null
) {
return previous !== current;
}
try {
return JSON.stringify(previous) !== JSON.stringify(current);
} catch {
// If value can't be serialized consistently, err on sending an update.
return true;
}
}
}
/**
* Create a comm bridge manager for an isolated frame.
*/
export function createCommBridgeManager(
options: CommBridgeManagerOptions,
): CommBridgeManager {
return new CommBridgeManager(options);
}
registry/outputs/isolated/IsolationTest.tsx
"use client";
import { useEffect, useRef, useState } from "react";
import { IsolatedFrame, type IsolatedFrameHandle } from "./isolated-frame";
import { IsolatedRendererProvider } from "./isolated-renderer-context";
/**
* Test results from the isolated iframe
*/
interface IsolationTestResult {
hasTauri: boolean;
hasInvoke: boolean;
canAccessParentDocument: boolean;
canAccessParentLocalStorage: boolean;
canUseOwnLocalStorage: boolean;
canUseOwnCookies: boolean;
canUseIndexedDB: boolean;
canFetchParentOrigin: boolean;
windowOrigin: string;
error?: string;
}
/**
* HTML template for the isolation test iframe.
* This runs inside the iframe and reports back via postMessage.
* Also handles bidirectional communication (ping/pong, eval, render).
*/
const ISOLATION_TEST_HTML = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {
font-family: system-ui, sans-serif;
padding: 16px;
margin: 0;
background: #1a1a1a;
color: #e0e0e0;
}
.test-item {
padding: 8px;
margin: 4px 0;
border-radius: 4px;
}
.pass { background: #1e3a1e; color: #4ade80; }
.fail { background: #3a1e1e; color: #f87171; }
.info { background: #1e2a3a; color: #60a5fa; }
pre { font-size: 12px; overflow: auto; }
#render-target { margin-top: 16px; padding: 8px; border: 1px dashed #444; min-height: 20px; }
</style>
</head>
<body>
<h3>Iframe Isolation Test</h3>
<div id="results"></div>
<div id="messages"></div>
<div id="render-target"></div>
<script>
const results = {
hasTauri: false,
hasInvoke: false,
canAccessParentDocument: false,
canAccessParentLocalStorage: false,
canUseOwnLocalStorage: false,
canUseOwnCookies: false,
canUseIndexedDB: false,
canFetchParentOrigin: false,
windowOrigin: window.origin || 'null',
error: null
};
// Test 1: Check for window.__TAURI__
try {
results.hasTauri = typeof window.__TAURI__ !== 'undefined';
} catch (e) {
results.hasTauri = false;
}
// Test 2: Check for invoke function
try {
results.hasInvoke = typeof window.__TAURI_INTERNALS__?.invoke === 'function' ||
typeof window.__TAURI__?.core?.invoke === 'function';
} catch (e) {
results.hasInvoke = false;
}
// Test 3: Try to access parent document
try {
const test = window.parent.document.body;
results.canAccessParentDocument = true;
} catch (e) {
results.canAccessParentDocument = false;
}
// Test 4: Try to access parent localStorage
try {
const test = window.parent.localStorage.getItem('test');
results.canAccessParentLocalStorage = true;
} catch (e) {
results.canAccessParentLocalStorage = false;
}
// Test 5: Try to use iframe's own localStorage (should fail with opaque origin)
try {
localStorage.setItem('isolation_test', 'test');
localStorage.removeItem('isolation_test');
results.canUseOwnLocalStorage = true;
} catch (e) {
results.canUseOwnLocalStorage = false;
}
// Test 6: Try to use cookies
try {
document.cookie = 'isolation_test=1';
results.canUseOwnCookies = document.cookie.includes('isolation_test');
} catch (e) {
results.canUseOwnCookies = false;
}
// Test 7: Try to use IndexedDB (should fail with opaque origin)
try {
const request = indexedDB.open('isolation_test', 1);
request.onsuccess = () => {
results.canUseIndexedDB = true;
request.result.close();
indexedDB.deleteDatabase('isolation_test');
updateResults();
};
request.onerror = () => {
results.canUseIndexedDB = false;
updateResults();
};
} catch (e) {
results.canUseIndexedDB = false;
}
// Test 8: Try to fetch parent origin (will test after display)
// This is async, so we'll update results later
// Display results in iframe
const container = document.getElementById('results');
const tests = [
{ name: 'window.__TAURI__ exists', value: results.hasTauri, expectFalse: true },
{ name: 'invoke() accessible', value: results.hasInvoke, expectFalse: true },
{ name: 'Can access parent document', value: results.canAccessParentDocument, expectFalse: true },
{ name: 'Can access parent localStorage', value: results.canAccessParentLocalStorage, expectFalse: true },
{ name: 'Can use own localStorage', value: results.canUseOwnLocalStorage, expectFalse: true, info: 'Opaque origin blocks storage' },
{ name: 'Can use cookies', value: results.canUseOwnCookies, expectFalse: true, info: 'Opaque origin blocks cookies' },
{ name: 'Can use IndexedDB', value: results.canUseIndexedDB, expectFalse: true, info: 'Opaque origin blocks IndexedDB' },
];
function updateResults() {
container.innerHTML = '';
const currentTests = [
{ name: 'window.__TAURI__ exists', value: results.hasTauri, expectFalse: true },
{ name: 'invoke() accessible', value: results.hasInvoke, expectFalse: true },
{ name: 'Can access parent document', value: results.canAccessParentDocument, expectFalse: true },
{ name: 'Can access parent localStorage', value: results.canAccessParentLocalStorage, expectFalse: true },
{ name: 'Can use own localStorage', value: results.canUseOwnLocalStorage, expectFalse: true },
{ name: 'Can use cookies', value: results.canUseOwnCookies, expectFalse: true },
{ name: 'Can use IndexedDB', value: results.canUseIndexedDB, expectFalse: true },
{ name: 'Can fetch parent origin', value: results.canFetchParentOrigin, expectFalse: true },
];
currentTests.forEach(test => {
const pass = test.expectFalse ? !test.value : test.value;
const div = document.createElement('div');
div.className = 'test-item ' + (pass ? 'pass' : 'fail');
div.textContent = (pass ? '✓ ' : '✗ ') + test.name + ': ' + test.value;
container.appendChild(div);
});
const originDiv = document.createElement('div');
originDiv.className = 'test-item info';
originDiv.innerHTML = '<pre>Window origin: ' + results.windowOrigin + '</pre>';
container.appendChild(originDiv);
window.parent.postMessage({ type: 'isolation_test_result', results }, '*');
}
tests.forEach(test => {
const pass = test.expectFalse ? !test.value : test.value;
const div = document.createElement('div');
div.className = 'test-item ' + (pass ? 'pass' : 'fail');
div.textContent = (pass ? '✓ ' : '✗ ') + test.name + ': ' + test.value;
container.appendChild(div);
});
const originDiv = document.createElement('div');
originDiv.className = 'test-item info';
originDiv.innerHTML = '<pre>Window origin: ' + results.windowOrigin + '</pre>';
container.appendChild(originDiv);
// Test fetch to parent origin (async)
if (window.parent !== window) {
let parentOrigin = '/';
try {
parentOrigin = window.parent.location?.origin || '/';
} catch {
// Expected in sandboxed iframe - origin access blocked
}
fetch(parentOrigin, { mode: 'cors' })
.then(() => {
results.canFetchParentOrigin = true;
updateResults();
})
.catch(() => {
results.canFetchParentOrigin = false;
updateResults();
});
}
// Send initial results to parent
window.parent.postMessage({ type: 'isolation_test_result', results }, '*');
// --- Bidirectional Communication ---
const messagesContainer = document.getElementById('messages');
const renderTarget = document.getElementById('render-target');
function logMessage(text, type = 'info') {
const div = document.createElement('div');
div.className = 'test-item ' + type;
div.textContent = text;
messagesContainer.appendChild(div);
}
// Listen for messages from parent
window.addEventListener('message', function(event) {
// Only accept messages from our parent window
if (event.source !== window.parent) {
return;
}
const { type, payload } = event.data || {};
switch (type) {
case 'ping':
// Respond to ping with pong
logMessage('Received ping, sending pong...', 'info');
window.parent.postMessage({
type: 'pong',
payload: {
receivedAt: Date.now(),
echo: payload
}
}, '*');
break;
case 'eval':
// Bootstrap/eval pattern (like Colab)
logMessage('Received eval command', 'info');
window.currentMessage = event;
try {
const result = eval.call(null, payload.code);
window.parent.postMessage({
type: 'eval_result',
payload: { success: true, result: String(result) }
}, '*');
} catch (e) {
window.parent.postMessage({
type: 'eval_result',
payload: { success: false, error: e.message }
}, '*');
} finally {
delete window.currentMessage;
}
break;
case 'render':
// Render HTML content
logMessage('Received render command: ' + payload.mimeType, 'info');
if (payload.mimeType === 'text/html') {
const range = document.createRange();
const fragment = range.createContextualFragment(payload.data);
renderTarget.innerHTML = '';
renderTarget.appendChild(fragment);
window.parent.postMessage({ type: 'render_complete' }, '*');
}
break;
default:
logMessage('Unknown message type: ' + type, 'fail');
}
});
// Notify parent that iframe is ready for communication
window.parent.postMessage({ type: 'ready' }, '*');
</script>
</body>
</html>`;
/**
* IsolationTest component - A proof-of-concept to verify that blob URL iframes
* are properly isolated from Tauri's IPC injection.
*
* This component creates an iframe using a blob: URL with sandbox attributes
* that should prevent access to Tauri APIs while still allowing script execution.
*
* Expected results for proper isolation:
* - window.__TAURI__ should be undefined
* - invoke() should not be accessible
* - Parent document should not be accessible
* - Parent localStorage should not be accessible
*/
/**
* Communication test state
*/
interface CommTestState {
iframeReady: boolean;
pingCount: number;
lastPongTime: number | null;
evalResult: string | null;
renderComplete: boolean;
}
export function IsolationTest() {
const iframeRef = useRef<HTMLIFrameElement>(null);
const [blobUrl, setBlobUrl] = useState<string | null>(null);
const [testResult, setTestResult] = useState<IsolationTestResult | null>(
null,
);
const [parentHasTauri, setParentHasTauri] = useState<boolean>(false);
const [commState, setCommState] = useState<CommTestState>({
iframeReady: false,
pingCount: 0,
lastPongTime: null,
evalResult: null,
renderComplete: false,
});
// Check if parent has Tauri (for comparison)
useEffect(() => {
setParentHasTauri(
typeof (window as unknown as { __TAURI__?: unknown }).__TAURI__ !==
"undefined",
);
}, []);
// Create blob URL on mount
useEffect(() => {
const blob = new Blob([ISOLATION_TEST_HTML], { type: "text/html" });
const url = URL.createObjectURL(blob);
setBlobUrl(url);
return () => {
URL.revokeObjectURL(url);
};
}, []);
// Listen for messages from iframe
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
const { type, payload } = event.data || {};
switch (type) {
case "isolation_test_result":
setTestResult(payload ?? event.data.results);
break;
case "ready":
setCommState((prev) => ({ ...prev, iframeReady: true }));
break;
case "pong":
setCommState((prev) => ({
...prev,
pingCount: prev.pingCount + 1,
lastPongTime: payload?.receivedAt ?? Date.now(),
}));
break;
case "eval_result":
setCommState((prev) => ({
...prev,
evalResult: payload?.success
? `Success: ${payload.result}`
: `Error: ${payload?.error}`,
}));
break;
case "render_complete":
setCommState((prev) => ({ ...prev, renderComplete: true }));
break;
}
};
window.addEventListener("message", handleMessage);
return () => window.removeEventListener("message", handleMessage);
}, []);
// Send message to iframe
const sendToIframe = (type: string, payload?: unknown) => {
if (iframeRef.current?.contentWindow) {
iframeRef.current.contentWindow.postMessage({ type, payload }, "*");
}
};
const handlePing = () => {
sendToIframe("ping", { sentAt: Date.now() });
};
const handleEval = () => {
sendToIframe("eval", { code: "1 + 2 + 3" });
};
const handleRender = () => {
setCommState((prev) => ({ ...prev, renderComplete: false }));
sendToIframe("render", {
mimeType: "text/html",
data: "<div style='color: #4ade80; padding: 8px;'>HTML rendered via postMessage!</div>",
});
};
const isIsolated =
testResult &&
!testResult.hasTauri &&
!testResult.hasInvoke &&
!testResult.canAccessParentDocument &&
!testResult.canAccessParentLocalStorage &&
!testResult.canUseOwnLocalStorage &&
!testResult.canUseOwnCookies &&
!testResult.canUseIndexedDB &&
!testResult.canFetchParentOrigin;
return (
<div
data-testid="isolation-test"
className="bg-background text-foreground space-y-4 p-4"
>
<h2 className="text-lg font-semibold">Blob URL Iframe Isolation Test</h2>
{/* Parent context info */}
<div className="bg-muted rounded p-3">
<h3 className="mb-2 font-medium">Parent Window Context:</h3>
<p className="text-sm">
window.__TAURI__ exists:{" "}
<span
className={parentHasTauri ? "text-yellow-500" : "text-green-500"}
>
{parentHasTauri ? "Yes (expected in Tauri app)" : "No"}
</span>
</p>
<p className="text-sm">
Window origin: <code className="text-xs">{window.origin}</code>
</p>
</div>
{/* Test results */}
{testResult && (
<div
className={`rounded p-3 ${
isIsolated
? "border border-green-700 bg-green-950"
: "border border-red-700 bg-red-950"
}`}
>
<h3 className="mb-2 font-medium">
{isIsolated ? "Iframe is properly isolated!" : "Isolation FAILED"}
</h3>
<ul className="space-y-1 text-sm">
<li>
Tauri API blocked:{" "}
<span
className={
!testResult.hasTauri ? "text-green-500" : "text-red-500"
}
>
{!testResult.hasTauri ? "Yes" : "No"}
</span>
</li>
<li>
invoke() blocked:{" "}
<span
className={
!testResult.hasInvoke ? "text-green-500" : "text-red-500"
}
>
{!testResult.hasInvoke ? "Yes" : "No"}
</span>
</li>
<li>
Parent document blocked:{" "}
<span
className={
!testResult.canAccessParentDocument
? "text-green-500"
: "text-red-500"
}
>
{!testResult.canAccessParentDocument ? "Yes" : "No"}
</span>
</li>
<li>
Parent localStorage blocked:{" "}
<span
className={
!testResult.canAccessParentLocalStorage
? "text-green-500"
: "text-red-500"
}
>
{!testResult.canAccessParentLocalStorage ? "Yes" : "No"}
</span>
</li>
<li>
Own localStorage blocked (opaque origin):{" "}
<span
className={
!testResult.canUseOwnLocalStorage
? "text-green-500"
: "text-red-500"
}
>
{!testResult.canUseOwnLocalStorage ? "Yes" : "No"}
</span>
</li>
<li>
Cookies blocked (opaque origin):{" "}
<span
className={
!testResult.canUseOwnCookies
? "text-green-500"
: "text-red-500"
}
>
{!testResult.canUseOwnCookies ? "Yes" : "No"}
</span>
</li>
<li>
IndexedDB blocked (opaque origin):{" "}
<span
className={
!testResult.canUseIndexedDB
? "text-green-500"
: "text-red-500"
}
>
{!testResult.canUseIndexedDB ? "Yes" : "No"}
</span>
</li>
<li>
Parent origin fetch blocked:{" "}
<span
className={
!testResult.canFetchParentOrigin
? "text-green-500"
: "text-red-500"
}
>
{!testResult.canFetchParentOrigin ? "Yes" : "No"}
</span>
</li>
<li>
Iframe origin:{" "}
<code className="text-xs">{testResult.windowOrigin}</code>
</li>
</ul>
</div>
)}
{/* Communication Test Controls */}
<div className="bg-muted space-y-3 rounded p-3">
<h3 className="font-medium">Bidirectional Communication Test:</h3>
<div className="flex items-center gap-2 text-sm">
<span>
Iframe ready:{" "}
<span
className={
commState.iframeReady ? "text-green-500" : "text-yellow-500"
}
>
{commState.iframeReady ? "Yes" : "Waiting..."}
</span>
</span>
</div>
<div className="flex flex-wrap gap-2">
<button
onClick={handlePing}
disabled={!commState.iframeReady}
className="rounded bg-blue-600 px-3 py-1.5 text-sm hover:bg-blue-700 disabled:bg-gray-600"
>
Send Ping
</button>
<button
onClick={handleEval}
disabled={!commState.iframeReady}
className="rounded bg-purple-600 px-3 py-1.5 text-sm hover:bg-purple-700 disabled:bg-gray-600"
>
Test Eval (1+2+3)
</button>
<button
onClick={handleRender}
disabled={!commState.iframeReady}
className="rounded bg-green-600 px-3 py-1.5 text-sm hover:bg-green-700 disabled:bg-gray-600"
>
Test Render HTML
</button>
</div>
<div className="space-y-1 text-sm">
<p>Pong responses received: {commState.pingCount}</p>
{commState.evalResult && <p>Eval result: {commState.evalResult}</p>}
{commState.renderComplete && (
<p className="text-green-500">Render completed successfully</p>
)}
</div>
</div>
{/* The actual isolated iframe */}
{blobUrl && (
<div className="overflow-hidden rounded border">
<iframe
ref={iframeRef}
src={blobUrl}
sandbox="allow-scripts"
className="h-80 w-full bg-neutral-900"
title="Isolation Test Frame"
/>
</div>
)}
{/* Sandbox attribute explanation */}
<div className="text-muted-foreground space-y-2 text-xs">
<p>
<strong>Sandbox attributes:</strong> allow-scripts (no
allow-same-origin)
</p>
<p>
Without <code>allow-same-origin</code>, the iframe gets an{" "}
<strong>opaque origin</strong> which:
</p>
<ul className="ml-4 list-disc space-y-1">
<li>Cannot access parent document, localStorage, or cookies</li>
<li>Cannot use its own localStorage, cookies, or IndexedDB</li>
<li>Blocks Tauri IPC injection (Tauri only injects at app origin)</li>
</ul>
<p className="border-l-2 border-yellow-600 bg-yellow-950/30 p-2">
<strong>⚠️ Web Security Note:</strong> On the web, blob URLs inherit
the creator's origin. The sandbox attribute creates isolation,
but for maximum security in production web apps, serve untrusted
content from a <strong>separate domain</strong> (e.g.,{" "}
<code>runtusercontent.com</code>) rather than a subdomain, as
subdomains can share cookies in some configurations.
</p>
</div>
{/* Production IsolatedFrame Demo */}
<ProductionFrameDemo />
</div>
);
}
/**
* Demo of the production IsolatedFrame component.
*/
function ProductionFrameDemo() {
const frameRef = useRef<IsolatedFrameHandle>(null);
const [isReady, setIsReady] = useState(false);
const [height, setHeight] = useState(0);
const handleRenderHtml = () => {
frameRef.current?.render({
mimeType: "text/html",
data: `
<h2 style="margin: 0 0 8px 0;">Production IsolatedFrame Test</h2>
<p>This content was rendered via the <code>IsolatedFrame</code> component.</p>
<table>
<tr><th>Feature</th><th>Status</th></tr>
<tr><td>Blob URL isolation</td><td style="color: #4ade80;">Works</td></tr>
<tr><td>postMessage communication</td><td style="color: #4ade80;">Works</td></tr>
<tr><td>Auto-resizing</td><td style="color: #4ade80;">Works</td></tr>
</table>
<script>console.log('Script executed in isolated frame!');</script>
`,
});
};
const handleRenderImage = () => {
// A small test image (1x1 red pixel in base64)
frameRef.current?.render({
mimeType: "image/png",
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==",
});
};
const handleClear = () => {
frameRef.current?.clear();
};
return (
<IsolatedRendererProvider basePath="/isolated">
<div className="bg-muted border-border mt-4 space-y-3 rounded border-t p-3">
<h3 className="font-medium">Production IsolatedFrame Component:</h3>
<div className="flex items-center gap-2 text-sm">
<span>
Ready:{" "}
<span className={isReady ? "text-green-500" : "text-yellow-500"}>
{isReady ? "Yes" : "Waiting..."}
</span>
</span>
<span className="text-muted-foreground">|</span>
<span>Height: {height}px</span>
</div>
<div className="flex flex-wrap gap-2">
<button
onClick={handleRenderHtml}
disabled={!isReady}
className="rounded bg-blue-600 px-3 py-1.5 text-sm hover:bg-blue-700 disabled:bg-gray-600"
>
Render HTML
</button>
<button
onClick={handleRenderImage}
disabled={!isReady}
className="rounded bg-purple-600 px-3 py-1.5 text-sm hover:bg-purple-700 disabled:bg-gray-600"
>
Render Image
</button>
<button
onClick={handleClear}
disabled={!isReady}
className="rounded bg-red-600 px-3 py-1.5 text-sm hover:bg-red-700 disabled:bg-gray-600"
>
Clear
</button>
</div>
<div className="overflow-hidden rounded border">
<IsolatedFrame
ref={frameRef}
darkMode={true}
minHeight={48}
maxHeight={400}
onReady={() => setIsReady(true)}
onResize={setHeight}
onLinkClick={(url, newTab) => {
console.log("Link clicked:", url, newTab);
window.open(url, newTab ? "_blank" : "_self");
}}
onError={(err) => console.error("Frame error:", err)}
/>
</div>
</div>
</IsolatedRendererProvider>
);
}
registry/outputs/isolated/isolated-renderer-context.tsx
"use client";
import {
createContext,
type ReactNode,
useContext,
useEffect,
useState,
} from "react";
interface IsolatedRendererBundle {
rendererCode: string;
rendererCss: string;
}
interface IsolatedRendererContextValue {
rendererCode: string | undefined;
rendererCss: string | undefined;
isLoading: boolean;
error: Error | null;
}
const IsolatedRendererContext =
createContext<IsolatedRendererContextValue | null>(null);
interface IsolatedRendererProviderProps {
children: ReactNode;
/** Base path to fetch isolated-renderer.js and isolated-renderer.css from */
basePath?: string;
/** Custom loader function (e.g., for Vite virtual modules) */
loader?: () => Promise<IsolatedRendererBundle>;
}
// Module-level cache (shared across all provider instances)
let bundleCache: IsolatedRendererBundle | null = null;
let loadingPromise: Promise<IsolatedRendererBundle> | null = null;
/**
* Provider for the isolated renderer bundle.
*
* Wrap your app (or the part that uses IsolatedFrame) with this provider.
*
* @example
* // Option A: Fetch from a URL path
* <IsolatedRendererProvider basePath="/isolated">
* <App />
* </IsolatedRendererProvider>
*
* @example
* // Option B: Use Vite virtual module (for Tauri/bundled apps)
* <IsolatedRendererProvider loader={() => import("virtual:isolated-renderer")}>
* <App />
* </IsolatedRendererProvider>
*/
export function IsolatedRendererProvider({
children,
basePath,
loader,
}: IsolatedRendererProviderProps) {
const [state, setState] = useState<IsolatedRendererContextValue>(() => ({
rendererCode: bundleCache?.rendererCode,
rendererCss: bundleCache?.rendererCss,
isLoading: !bundleCache,
error: null,
}));
useEffect(() => {
if (bundleCache) {
// Already loaded, update state if needed
if (state.isLoading) {
setState({
rendererCode: bundleCache.rendererCode,
rendererCss: bundleCache.rendererCss,
isLoading: false,
error: null,
});
}
return;
}
let cancelled = false;
if (!loadingPromise) {
if (loader) {
// Use custom loader (Vite plugin, etc.)
loadingPromise = loader();
} else if (basePath) {
// Fetch from URL
loadingPromise = Promise.all([
fetch(`${basePath}/isolated-renderer.js`).then((r) => {
if (!r.ok)
throw new Error(`Failed to fetch renderer JS: ${r.status}`);
return r.text();
}),
fetch(`${basePath}/isolated-renderer.css`).then((r) => {
if (!r.ok)
throw new Error(`Failed to fetch renderer CSS: ${r.status}`);
return r.text();
}),
]).then(([js, css]) => ({ rendererCode: js, rendererCss: css }));
} else {
const error = new Error(
"IsolatedRendererProvider requires either 'basePath' or 'loader' prop. " +
"See: https://elements.nteract.io/docs/outputs/isolated-frame#setup",
);
setState((s) => ({ ...s, isLoading: false, error }));
return;
}
}
loadingPromise
.then((bundle) => {
bundleCache = bundle;
if (!cancelled) {
setState({
rendererCode: bundle.rendererCode,
rendererCss: bundle.rendererCss,
isLoading: false,
error: null,
});
}
})
.catch((error) => {
console.error("[IsolatedRendererProvider] Bundle load failed:", error);
loadingPromise = null; // Allow retry on next mount
if (!cancelled) {
setState((s) => ({ ...s, isLoading: false, error }));
}
});
return () => {
cancelled = true;
};
}, [basePath, loader, state.isLoading]);
return (
<IsolatedRendererContext.Provider value={state}>
{children}
</IsolatedRendererContext.Provider>
);
}
// Default state when no provider is present (e.g., during SSR)
const NO_PROVIDER_STATE: IsolatedRendererContextValue = {
rendererCode: undefined,
rendererCss: undefined,
isLoading: true,
error: null,
};
/**
* Hook to access the isolated renderer bundle.
*
* Returns a "loading" state if used outside IsolatedRendererProvider,
* which allows components to render safely during SSR.
* In development, logs a warning when no provider is present.
*/
export function useIsolatedRenderer(): IsolatedRendererContextValue {
const context = useContext(IsolatedRendererContext);
if (!context) {
// During SSR or when provider is missing, return a "not ready" state
// This allows components to render without crashing
if (
process.env.NODE_ENV === "development" &&
typeof window !== "undefined"
) {
console.warn(
"useIsolatedRenderer: No IsolatedRendererProvider found. " +
"Wrap your app with <IsolatedRendererProvider>. " +
"See: https://elements.nteract.io/docs/outputs/isolated-frame#setup",
);
}
return NO_PROVIDER_STATE;
}
return context;
}
/**
* Reset the bundle cache (useful for testing).
* @internal
*/
export function _resetBundleCache() {
bundleCache = null;
loadingPromise = null;
}