import { createRecursiveProxy } from "@trpc/server/shared"; import invariant from "tiny-invariant"; import { toast } from "@webstudio-is/design-system"; import { uploadAssets } from "~/builder/shared/assets/upload-assets"; import { showTokenConflictDialog } from "./token-conflict-dialog"; const apiWindowNamespace = "__webstudio__$__builderApi"; type ToastHandler = (message: string) => void; const isSafeMode = (() => { if (typeof window === "undefined") { return false; } return new URLSearchParams(window.location.search).get("safemode") === "true"; })(); const _builderApi = { isInitialized: () => true, isSafeMode: () => isSafeMode, toast: { info: toast.info as ToastHandler, warn: toast.warn as ToastHandler, error: toast.error as ToastHandler, success: toast.success as ToastHandler, }, uploadImages: async (srcs: string[]) => { const urlToIds = await uploadAssets( "image", srcs.map((src) => new URL(src)) ); return new Map([...urlToIds.entries()].map(([url, id]) => [url.href, id])); }, showTokenConflictDialog, }; declare global { interface Window { [apiWindowNamespace]: typeof _builderApi; } } const isInTop = () => { try { return window.self === window.top; } catch { return true; } }; const getTopApi = () => { if (isInTop()) { // Inside the iframe, use the local window.api return _builderApi; } else { // Find first iframe with the API invariant(window.top); return window.top[apiWindowNamespace]; } }; const isKeyOf = (key: unknown, obj: T): key is keyof T => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore return key in obj; }; /** * Forwards the call from the builder to the iframe, invoking the original API in the iframe. */ export const builderApi = createRecursiveProxy((options) => { const api = getTopApi(); if (api == null) { if ( options.path.join(".") === ("isInitialized" satisfies keyof typeof _builderApi) ) { return false; } console.warn( `API not found in the iframe, skipping ${options.path.join(".")} call, iframe probably not loaded yet` ); return null; } let currentMethod = api as unknown; for (const key of options.path) { invariant( isKeyOf(key, currentMethod), `API method ${options.path.join(".")} not found` ); invariant(typeof currentMethod === "object"); invariant(currentMethod != null); currentMethod = currentMethod[key]; } invariant( typeof currentMethod === "function", `API method ${options.path.join(".")} is not a function` ); return currentMethod.call(null, ...options.args); }) as typeof _builderApi; /** * Initializes the builder API in the window. Must be called in the builder context. */ export const initBuilderApi = () => { if (isInTop()) { window[apiWindowNamespace] = _builderApi; } return () => {}; };