interface CreateComponentInstanceParams {
componentKey: string;
x?: number;
y?: number;
parentId?: string;
}
interface GetInstanceOverridesParams {
instanceNodeId?: string;
}
interface SetInstanceOverridesParams {
sourceInstanceId: string;
targetNodeIds: string[];
}
/**
* Get local components
*/
export async function getLocalComponents(): Promise<
Array<{
id: string;
name: string;
key: string;
description: string;
}>
> {
const components: Array<{
id: string;
name: string;
key: string;
description: string;
}> = [];
function findComponents(node: BaseNode): void {
if (node.type === "COMPONENT") {
const component = node as ComponentNode;
components.push({
id: component.id,
name: component.name,
key: component.key,
description: component.description,
});
}
if ("children" in node) {
const parent = node as ChildrenMixin;
for (const child of parent.children) {
findComponents(child);
}
}
}
// Search in current page
findComponents(figma.currentPage);
return components;
}
/**
* Create a component instance
*/
export async function createComponentInstance(
params: CreateComponentInstanceParams,
): Promise<{
id: string;
name: string;
x: number;
y: number;
mainComponentId: string;
}> {
const { componentKey, x = 0, y = 0, parentId } = params;
// Try to find component by key
const component = await figma.importComponentByKeyAsync(componentKey);
if (!component) {
throw new Error(`Component not found with key: ${componentKey}`);
}
const instance = component.createInstance();
instance.x = x;
instance.y = y;
if (parentId) {
const parentNode = await figma.getNodeByIdAsync(parentId);
if (!parentNode) {
throw new Error(`Parent node not found with ID: ${parentId}`);
}
if (!("appendChild" in parentNode)) {
throw new Error(`Parent node does not support children: ${parentId}`);
}
(parentNode as ChildrenMixin).appendChild(instance);
} else {
figma.currentPage.appendChild(instance);
}
return {
id: instance.id,
name: instance.name,
x: instance.x,
y: instance.y,
mainComponentId: component.id,
};
}
/**
* Get instance overrides
*/
export async function getInstanceOverrides(
params: GetInstanceOverridesParams,
): Promise<{
success: boolean;
instanceId: string;
instanceName: string;
overrides: Record<string, unknown>;
}> {
let instance: InstanceNode | null = null;
if (params.instanceNodeId) {
const node = await figma.getNodeByIdAsync(params.instanceNodeId);
if (!node) {
throw new Error(`Node not found with ID: ${params.instanceNodeId}`);
}
if (node.type !== "INSTANCE") {
throw new Error(`Node is not an instance: ${params.instanceNodeId}`);
}
instance = node as InstanceNode;
} else {
// Use first selected instance
const selection = figma.currentPage.selection;
const selectedInstance = selection.find((n) => n.type === "INSTANCE") as
| InstanceNode
| undefined;
if (!selectedInstance) {
throw new Error("No instance selected");
}
instance = selectedInstance;
}
// Get all overridden properties
const overrides: Record<string, unknown> = {};
// Collect text overrides
function collectOverrides(node: SceneNode, path: string = ""): void {
const currentPath = path ? `${path}/${node.name}` : node.name;
if (node.type === "TEXT") {
const textNode = node as TextNode;
overrides[currentPath] = {
type: "TEXT",
characters: textNode.characters,
};
}
if ("children" in node) {
const parent = node as ChildrenMixin;
for (const child of parent.children as SceneNode[]) {
collectOverrides(child, currentPath);
}
}
}
collectOverrides(instance);
return {
success: true,
instanceId: instance.id,
instanceName: instance.name,
overrides,
};
}
/**
* Set instance overrides
*/
export async function setInstanceOverrides(
params: SetInstanceOverridesParams,
): Promise<{
success: boolean;
appliedCount: number;
failedCount: number;
results: Array<{ targetId: string; success: boolean; error?: string }>;
}> {
const { sourceInstanceId, targetNodeIds } = params;
// Get source instance
const sourceNode = await figma.getNodeByIdAsync(sourceInstanceId);
if (!sourceNode || sourceNode.type !== "INSTANCE") {
throw new Error(`Source instance not found: ${sourceInstanceId}`);
}
const sourceInstance = sourceNode as InstanceNode;
// Collect source overrides
const sourceOverrides: Map<string, { characters: string }> = new Map();
function collectTextOverrides(node: SceneNode, path: string = ""): void {
const currentPath = path ? `${path}/${node.name}` : node.name;
if (node.type === "TEXT") {
const textNode = node as TextNode;
sourceOverrides.set(currentPath, { characters: textNode.characters });
}
if ("children" in node) {
const parent = node as ChildrenMixin;
for (const child of parent.children as SceneNode[]) {
collectTextOverrides(child, currentPath);
}
}
}
collectTextOverrides(sourceInstance);
// Apply overrides to targets
const results: Array<{ targetId: string; success: boolean; error?: string }> =
[];
for (const targetId of targetNodeIds) {
try {
const targetNode = await figma.getNodeByIdAsync(targetId);
if (!targetNode || targetNode.type !== "INSTANCE") {
results.push({ targetId, success: false, error: "Not an instance" });
continue;
}
const targetInstance = targetNode as InstanceNode;
// Apply text overrides
async function applyTextOverrides(
node: SceneNode,
path: string = "",
): Promise<void> {
const currentPath = path ? `${path}/${node.name}` : node.name;
if (node.type === "TEXT" && sourceOverrides.has(currentPath)) {
const textNode = node as TextNode;
const override = sourceOverrides.get(currentPath) ?? {
characters: "",
};
await figma.loadFontAsync(textNode.fontName as FontName);
textNode.characters = override.characters;
}
if ("children" in node) {
const parent = node as ChildrenMixin;
for (const child of parent.children as SceneNode[]) {
await applyTextOverrides(child, currentPath);
}
}
}
await applyTextOverrides(targetInstance);
results.push({ targetId, success: true });
} catch (error) {
results.push({
targetId,
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
}
return {
success: results.some((r) => r.success),
appliedCount: results.filter((r) => r.success).length,
failedCount: results.filter((r) => !r.success).length,
results,
};
}