Files
webstudio/apps/builder/app/shared/builder-api.ts
T
Bogdan Chadkin db95b55552 refactor: replace date-fns with use-relative-time (#4738)
See https://github.com/nkzw-tech/use-relative-time

We use only single function from date-fns while the package is like
22MB. Here's small alternative for react based on intl.

Added jsx support to ours toasts inside of builder and RelativeTime
component to use it inside of callbacks.
2025-01-11 20:26:16 +07:00

109 lines
2.6 KiB
TypeScript

import { createRecursiveProxy } from "@trpc/server/shared";
import invariant from "tiny-invariant";
import { toast } from "@webstudio-is/design-system";
import { uploadAssets } from "~/builder/shared/assets/use-assets";
const apiWindowNamespace = "__webstudio__$__builderApi";
type ToastHandler = (message: string) => void;
const _builderApi = {
isInitialized: () => true,
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]));
},
};
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 = <T>(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 () => {};
};