Skip to content

Instantly share code, notes, and snippets.

@yagudaev
Created July 6, 2023 18:27
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yagudaev/f8ba07012ef6ce8360b7543c56242eb1 to your computer and use it in GitHub Desktop.
Save yagudaev/f8ba07012ef6ce8360b7543c56242eb1 to your computer and use it in GitHub Desktop.
import { emit, on, once } from "@create-figma-plugin/utilities"
export type AsyncActionType<F extends (...args: any) => any> = F
export type SyncActionType<F extends (...args: any) => any> = (
...args: Parameters<F>
) => Promise<ReturnType<F>>
let lastCallerId = 0
let lastSubscriptionId = 0
const subscriptions = new Map<string, Function[]>()
export function callMainWithoutUnsubscribe(fnName: string, ...args: any[]) {
return call({ fnName, args, autoCleanupSubscriptions: false })
}
export function callMain(fnName: string, ...args: any[]) {
return call({ fnName, args })
}
function call({
fnName,
args,
autoCleanupSubscriptions = true
}: {
fnName: string
args: any[]
autoCleanupSubscriptions?: boolean
}) {
lastCallerId += 1
const callerId = lastCallerId
args = args.map((arg) => checkForCallbacks(fnName, callerId, arg))
function cleanUpSubscriptions() {
const unsubscribes = subscriptions.get(getSubscriptionStringPrefix(fnName, callerId))
unsubscribes?.forEach((unsubscribe) => unsubscribe())
subscriptions.set(getSubscriptionStringPrefix(fnName, callerId), [])
}
return new Promise<any>(function (resolve, reject) {
once(`RES_${fnName}_${callerId}`, (returnValue) => {
resolve(returnValue)
autoCleanupSubscriptions && cleanUpSubscriptions()
})
once(`ERR_${fnName}_${callerId}`, (error) => {
autoCleanupSubscriptions && cleanUpSubscriptions()
let errorObj
try {
const errorJson = JSON.parse(error)
if (typeof errorJson === "string") {
errorObj = new Error(errorJson)
} else {
errorObj = new Error(errorJson.message ?? "Unknown error")
}
errorObj.stack += `\n${errorJson.stack}`
} catch (error) {
errorObj = new Error("Unknown error")
}
reject(errorObj)
})
emit(`REQ_${fnName}`, callerId, ...args)
})
}
export function exposeToUI(fn: (...args: any[]) => any) {
const name = (fn as any).actionName as string
on(`REQ_${name}`, async (callerId: number, ...reqArgs: any[]) => {
reqArgs = reqArgs.map((arg) => checkForSubscriptions(name, callerId, arg))
try {
const returnValue = await fn(...reqArgs)
emit(`RES_${name}_${callerId}`, returnValue)
} catch (error) {
let errorStr
if (typeof error === "string") {
const errorObj = new Error(error)
errorStr = JSON.stringify(errorObj, Object.getOwnPropertyNames(errorObj))
} else {
errorStr = JSON.stringify(error, Object.getOwnPropertyNames(error))
}
emit(`ERR_${name}_${callerId}`, errorStr)
}
})
}
export function exposeAllToUI(actions: { [key: string]: (...args: any[]) => any }) {
Object.keys(actions).map((actionName: string) => {
const fn = actions[actionName] as any
fn.actionName = actionName // protect against minification
exposeToUI(fn)
})
}
// helper functions
function checkForCallbacks(fnName: string, callerId: number, arg: any) {
if (typeof arg === "object") {
return {
...Object.keys(arg).reduce((acc, key) => {
acc[key] = checkForCallbacks(fnName, callerId, arg[key])
return acc
}, {} as any)
}
}
if (typeof arg !== "function") return arg
const callback = arg as Function
lastSubscriptionId += 1
const subscriptionId = lastSubscriptionId
const unsubscribe = on(
getSubscriptionString(fnName, callerId, subscriptionId),
(subscriptionId: number, ...args: any[]) => {
// call original callback
callback(...args)
}
)
const prevSubscriptions = subscriptions.get(getSubscriptionStringPrefix(fnName, callerId)) || []
subscriptions.set(getSubscriptionStringPrefix(fnName, callerId), [
...prevSubscriptions,
unsubscribe
])
return { subscriptionId, fnName: arg.name, __SUBSCRIPTION__: true }
}
function getSubscriptionString(fnName: string, callerId: number, subscriptionId: number): string {
return `${getSubscriptionStringPrefix(fnName, callerId)}_${subscriptionId}`
}
function getSubscriptionStringPrefix(fnName: string, callerId: number): string {
return `SUB_${fnName}_${callerId}`
}
function checkForSubscriptions(fnName: string, callerId: number, arg: any) {
if (typeof arg === "object" && !arg.__SUBSCRIPTION__) {
return {
...Object.keys(arg).reduce((acc, key) => {
acc[key] = checkForSubscriptions(fnName, callerId, arg[key])
return acc
}, {} as any)
}
}
if (typeof arg !== "object" || !arg.__SUBSCRIPTION__) return arg
const { subscriptionId } = arg
return (...args: any[]) => {
emit(getSubscriptionString(fnName, callerId, subscriptionId), subscriptionId, ...args)
}
}
export const subscribeToSelectionChange = function (callback: (nodeIds: string[]) => void) {
figma.on("selectionchange", () => {
callback(figma.currentPage.selection.map((n) => n.id))
})
}
export const getSelection = () => {
return figma.currentPage.selection.map((n) => n.id)
}
import { exposeAllToUI } from "../shared/figmaRPC"
import * as actions from "./actions"
export default function () {
exposeAllToUI(actions)
showUI({ width: 400, height: 380, themeColors: true })
}
import { getSelection, subscribeToSelectionChange } from "./mainAPI"
function Plugin() {
useEffect(() => {
getSelection().then(handleSelectionChange)
subscribeToSelectionChange(handleSelectionChange)
}, [])
async function handleSelectionChange(selectedNodeIds: string[]) {
console.log("selectedNodeIds", selectedNodeIds)
}
return <div>Plugin Content here</div>
}
import {
AsyncActionType,
SyncActionType,
callMain,
callMainWithoutUnsubscribe
} from "../shared/figmaRPC"
import type * as Actions from "../main/actions"
export const subscribeToSelectionChange: SyncActionType<typeof Actions.subscribeToSelectionChange> =
async function (...args) {
return callMainWithoutUnsubscribe("subscribeToSelectionChange", ...args)
}
export const getSelection: SyncActionType<typeof Actions.getSelection> = function () {
return callMain("getSelection")
}
@yagudaev
Copy link
Author

yagudaev commented Jul 6, 2023

Quick and dirty starter docs for keeping track of selection using the figma rest API

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment