mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-10-29 16:17:39 +09:00
- **Restore points for agent work**: Create named restore points before major edits, review changes with a dry run, and safely revert project content if needed. - **Shared Slot authoring**: Agents can turn existing sections into shared Slots and insert linked copies across pages, including valid nested Slots. - **DTCG and Figma token imports**: Import design tokens and CSS variables through CLI, MCP, or direct paste, with aliases, modes, composite values, and mappings supported. - **Consistent token conflict handling**: Every paste workflow now offers the same preserve, rename, or merge choices instead of silently overwriting existing design tokens. - **Draft-page previews**: Agents can generate and inspect previews of draft pages without publishing them or exposing drafts in production builds. - **Safer MCP mutations**: Destructive operations require explicit confirmation, invalid raw patches and invented record IDs are rejected, and restore operations commit atomically. - **More dependable agent contracts**: New tools have complete runtime schemas, clearer validation errors, and consistent behavior across CLI, MCP, Builder, and server APIs.
121 lines
3.0 KiB
TypeScript
121 lines
3.0 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/upload-assets";
|
|
import { showTokenConflictDialog } from "./token-conflict-dialog";
|
|
import { showDesignTokenImportDialog } from "./design-token-import-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]));
|
|
},
|
|
showDesignTokenImportDialog,
|
|
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 = <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 () => {};
|
|
};
|