@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
JavaScript
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 };