UNPKG

@refore-ai/copy-to-design-sdk

Version:

Copy the HTML source and import it into the design platform via the plugin.

308 lines (302 loc) 9.92 kB
import { Thumbmark } from "@thumbmarkjs/thumbmarkjs"; import CryptoJS from "crypto-js"; import { nanoid } from "nanoid"; import { createFetch } from "ofetch"; import { withResolvers } from "radashi"; import { io } from "socket.io-client"; //#region package.json var version = "2.0.0"; //#endregion //#region src/enum.ts let Region = /* @__PURE__ */ function(Region$1) { Region$1["China"] = "china"; Region$1["World"] = "world"; return Region$1; }({}); let PlatformType = /* @__PURE__ */ function(PlatformType$1) { PlatformType$1["Figma"] = "figma"; PlatformType$1["MasterGo"] = "mastergo"; PlatformType$1["JSDesign"] = "jsdesign"; PlatformType$1["PixsoChina"] = "pixso-china"; return PlatformType$1; }({}); let ImportMode = /* @__PURE__ */ function(ImportMode$1) { ImportMode$1["Quick"] = "quick"; ImportMode$1["Interactive"] = "interactive"; return ImportMode$1; }({}); //#endregion //#region src/permission.ts async function getPermission(permission) { try { return (await navigator.permissions.query({ name: permission })).state; } catch { return "not-support"; } } //#endregion //#region src/socket-manager.ts /** * Socket connection manager for reusing socket.io connections */ var SocketManager = class { connections = /* @__PURE__ */ new Map(); activeSubscriptions = /* @__PURE__ */ new Map(); getSubscriptionKey(resourceType, resourceId) { return `${resourceType}:${resourceId}`; } /** * Get or create a socket connection for the given server */ async getOrCreateConnection(server, authPayload) { const existing = this.connections.get(server); if (existing && existing.socket.connected) { existing.refCount++; existing.lastUsed = Date.now(); return existing.socket; } const socket = io(server, { path: "/socket.io", transports: ["websocket"], auth: (cb) => { cb(authPayload); } }); socket.on("resource:subscribe", (event) => { const subscriptionKey = this.getSubscriptionKey(event.resourceType, event.resourceId); const subscription = this.activeSubscriptions.get(subscriptionKey); if (!subscription || !event.payload) return; if (event.payload.success) subscription.defer.resolve(event.payload); else subscription.defer.reject(/* @__PURE__ */ new Error("Socket operation failed")); }); await new Promise((resolve, reject) => { socket.on("connect", () => resolve()); socket.on("connect_error", (error) => reject(error)); }); const connectionInfo = { socket, server, refCount: 1, lastUsed: Date.now() }; this.connections.set(server, connectionInfo); return socket; } /** * Release a connection reference */ releaseConnection(server) { const connection = this.connections.get(server); if (!connection) return; connection.refCount--; connection.lastUsed = Date.now(); if (connection.refCount <= 0) setTimeout(() => { const current = this.connections.get(server); if (current && current.refCount <= 0 && Date.now() - current.lastUsed > 3e4) { current.socket.disconnect(); this.connections.delete(server); } }, 3e4); } /** * Subscribe to a resource with automatic cleanup */ subscribeToResource(socket, resourceType, resourceId) { const defer = withResolvers(); const subscriptionKey = this.getSubscriptionKey(resourceType, resourceId); const cleanup = () => { this.activeSubscriptions.delete(subscriptionKey); }; this.activeSubscriptions.set(subscriptionKey, { resourceType, resourceId, defer, cleanup }); socket.emit("resource:subscribe", { resourceType, resourceId }); return defer.promise; } /** * Unsubscribe from a resource */ unsubscribeFromResource(socket, resourceType, resourceId) { const subscriptionKey = this.getSubscriptionKey(resourceType, resourceId); socket.emit("resource:unsubscribe", { resourceType, resourceId }); const subscription = this.activeSubscriptions.get(subscriptionKey); if (subscription) subscription.cleanup(); } /** * Manually cleanup all connections and active subscriptions * Useful for cleanup when the instance is no longer needed */ cleanup() { for (const [_taskId, subscription] of this.activeSubscriptions) subscription.cleanup(); this.activeSubscriptions.clear(); for (const [_server, connection] of this.connections) connection.socket.disconnect(); this.connections.clear(); } }; //#endregion //#region src/index.ts const COPY_TO_DESIGN_SDK_RESOURCE_TYPE = "CopyToDesignSDK"; const DEFAULT_GET_SERVER = (region) => region === Region.World ? "https://api.demoway.com" : "https://api.demoway.cn"; var CopyToDesign = class { thumbmark = new Thumbmark(); visitorId = null; socketManager = new SocketManager(); $fetch = createFetch({ defaults: { onRequest: async (context) => { const headers = context.options.headers; const visitorId = await this.getVisitorId(); const payload = await this.getAuthorizationPayload(); context.options.query = { ...context.options.query, client: "copy-to-design-sdk", version, visitorId, appId: payload.appId }; headers.set("Authorization", `Bearer ${payload.accessToken}`); }, async onResponse(context) { if (context.error) return; const data = context.response?._data; if (!data || typeof data !== "object") return; if (data.error) context.error = data.error; else if ("data" in data && context.response?._data) context.response._data = data.data; } } }); constructor(options) { this.options = options; } getAuthorizationPayload() { return this.options.getAuthorizationPayload(); } getServerByRegion(region) { return (this.options?._server ?? DEFAULT_GET_SERVER)(region); } async getVisitorId() { if (!this.visitorId) this.visitorId = (await this.thumbmark.get()).thumbmark; return this.visitorId; } async generatePluginReceiveDataForHTML(options) { const { content, platform, importMode = ImportMode.Interactive, width, height, attrs = {}, topLayerName, copyInfo } = options; const server = this.getServerByRegion(this.options.region); const source = JSON.stringify({ type: "html", html: content, importMode, width, height }); const secret = nanoid(32); const encrypted = CryptoJS.AES.encrypt(source, secret); const res = await this.$fetch("/api/refore/copy-to-design-v2/save-copy-info", { baseURL: server, method: "POST", body: { ...copyInfo, secret, platform } }); const div = document.createElement("refore-copy-to-design"); div.setAttribute("data-copy-id", res.copyId); div.setAttribute("data-copy-sdk-version", version); div.setAttribute("data-copy-server", server); div.setAttribute("data-copy-content", encrypted.toString()); const resolvedTopLayerName = Object.assign({ referrer: location.origin }, topLayerName); div.setAttribute("data-copy-top-layer-name", JSON.stringify(resolvedTopLayerName)); for (const key of Object.keys(attrs)) div.setAttribute(key, attrs[key]); return div.outerHTML; } async writeToClipboard(clipboardItem, options = {}) { const { onWaitingForFocus, onUserActionRequired } = options; if (!document.hasFocus()) { onWaitingForFocus?.(); const focusDefer = withResolvers(); const handleFocus = () => { window.removeEventListener("focus", handleFocus); focusDefer.resolve(); }; window.addEventListener("focus", handleFocus); await focusDefer.promise; } const permission = await getPermission("clipboard-write"); if (permission === "denied") throw new Error("clipboard-write permission is denied"); const writeClipboardDefer = withResolvers(); const writeClipboard = async () => { try { await navigator.clipboard.write([clipboardItem]); writeClipboardDefer.resolve(); } catch (err) { writeClipboardDefer.reject(err); } }; if (permission !== "not-support") await writeClipboard(); else onUserActionRequired?.(writeClipboard); await writeClipboardDefer.promise; } async preparePasteInPlugin(options) { const data = await this.generatePluginReceiveDataForHTML(options); return new ClipboardItem({ "text/html": new Blob([data], { type: "text/html" }) }); } async copyPasteInPlugin(options) { const clipboardItem = await this.preparePasteInPlugin(options); await this.writeToClipboard(clipboardItem, options); } /** * @deprecated * use `copyPasteInPlugin` instead */ async copyToClipboardFromHTML(html, options) { return this.copyPasteInPlugin({ content: html, ...options }); } async preparePasteDirect(options) { const { platform, ...rest } = options; const server = this.getServerByRegion(this.options.region); let connection = null; let taskId = null; try { const authorizationPayload = await this.getAuthorizationPayload(); connection = await this.socketManager.getOrCreateConnection(server, { accessToken: authorizationPayload.accessToken }); const pluginReceiveData = await this.generatePluginReceiveDataForHTML({ importMode: ImportMode.Quick, platform, ...rest, attrs: { ...rest.attrs, "data-rpa": "true" } }); ({taskId} = await this.$fetch("/api/refore/copy-to-design-v2/generate-paste-direct-data", { baseURL: server, method: "POST", body: { platform, content: pluginReceiveData } })); const res = await this.socketManager.subscribeToResource(connection, COPY_TO_DESIGN_SDK_RESOURCE_TYPE, taskId); return new ClipboardItem({ "text/html": new Blob([res.content], { type: "text/html" }) }); } finally { if (connection && taskId) this.socketManager.unsubscribeFromResource(connection, COPY_TO_DESIGN_SDK_RESOURCE_TYPE, taskId); if (connection) this.socketManager.releaseConnection(server); } } async copyPasteDirect(options) { const clipboardItem = await this.preparePasteDirect(options); await this.writeToClipboard(clipboardItem, options); } }; //#endregion export { CopyToDesign, ImportMode, PlatformType, Region };