Skip to content

Instantly share code, notes, and snippets.

@jurijsk
Created July 25, 2024 17:00
Show Gist options
  • Save jurijsk/14d92b8f612ed4364389be6b82d244ec to your computer and use it in GitHub Desktop.
Save jurijsk/14d92b8f612ed4364389be6b82d244ec to your computer and use it in GitHub Desktop.
Figma Plugin Styles to Local Variables swap
async function run() {
console.clear();
await figma.currentPage.loadAsync();
const dryRun = false;
const skipDevReady = true;
console.log(dryRun ? 'DRY RUN' : 'NORMAL RUN');
//setup allowed to use team libraries
const libraryNameAllowList = new Map<string, boolean>([['New Jobrad & Dealers', true]]);
const allowAllLibraries = false;
//get available library collections
let libraryVariableCollections = await figma.teamLibrary.getAvailableLibraryVariableCollectionsAsync();
//console.log('total collections count: ' + libraryVariableCollections.length);
const colorVariablesMap = new Map<string, Variable>();
for (let i = 0; i < libraryVariableCollections.length; i++) {
const collection = libraryVariableCollections[i];
const libFiler = allowAllLibraries || libraryNameAllowList.get(collection.libraryName) || false;
if (!libFiler) {
continue;
}
const libraryVariables = await figma.teamLibrary.getVariablesInLibraryCollectionAsync(collection.key);
for (let j = 0; j < libraryVariables.length; j++) {
const libraryVariable = libraryVariables[j];
if (libraryVariable.resolvedType === 'COLOR') {
const variable = await figma.variables.importVariableByKeyAsync(libraryVariable.key);
if (!variable) {
console.log('could not import variable', libraryVariable.key, libraryVariable.name);
continue;
}
if (colorVariablesMap.has(variable.name)) {
console.log(`DUPLICATE color variable named '${variable.name}'. Latest found value will be used.`);
}
colorVariablesMap.set(variable.name, variable);
//console.log(variable.name, variable);
}
}
}
const localVariables = await figma.variables.getLocalVariablesAsync('COLOR');
for (let i = 0; i < localVariables.length; i++) {
const localVariable = localVariables[i];
if (colorVariablesMap.has(localVariable.name)) {
console.log(`Ducplicate color variable named '${localVariable.name}'. Latest found value will be used.`);
}
colorVariablesMap.set(localVariable.name, localVariable);
}
const paintStyles = await figma.getLocalPaintStylesAsync();
console.log(paintStyles.length);
for (let i = 0; i < paintStyles.length; i++) {
const paintStyle = paintStyles[i];
console.log(paintStyle.name, paintStyle.id);
}
const components = figma.currentPage.findAllWithCriteria({ types: ['COMPONENT'] });
const notFountColorVariables = new Array<string>();
for (let i = 0; i < components.length; i++) {
const component = <ComponentNode> components[i];
let parent = component.parent;
let path = '';
if(parent.type === 'COMPONENT_SET'){
path = parent.name + '/';
}
path += component.name;
if (skipDevReady && component.devStatus && component.devStatus.type === 'READY_FOR_DEV') {
console.log('skipping dev ready component: ', component.name);
continue;
}
//console.log('processing component: ', component.name);
try {
await traverse(component, path, async (node: SceneNode, path: string) => {
//console.log('traverse callback: ', path);
let css: { [key: string]: string };
try {
try {
css = await node.getCSSAsync();
//console.log('css for node: ', path, css);
} catch (e) {
console.log('error getting CSS for node: ', path, node.type);
return;
}
let variableName = getVariableName(css["background"]);
if(!variableName){
variableName = getVariableName(css["color"]);
}
if (variableName) {
let color = colorVariablesMap.get(variableName);
if (!color) {
//console.log('color variable not found in linked team libraries: ', variableName);
notFountColorVariables.push(variableName);
return;
}
//console.log('setting backgorund color for: ', path, 'var name: ', variableName);
if (dryRun) {
return;
}
let fillable = node as MinimalFillsMixin;
const fillsCopy = clone(fillable.fills)
// Fills and strokes must be set via their immutable arrays
fillsCopy[0] = figma.variables.setBoundVariableForPaint(fillsCopy[0], 'color', color)
fillable.fills = fillsCopy;
console.log('SETTING backgorund color for: ', path, 'var name: ', variableName, fillable.fills);
}
let cssBorder = css["border"];
if(cssBorder){
console.log('border found:', cssBorder, getVariableName(cssBorder));
}
variableName = getVariableName(cssBorder);
if (variableName) {
console.log('border found:', variableName);
let color = colorVariablesMap.get(variableName);
if (!color) {
console.log('color variable not found in linked team libraries: ', variableName);
notFountColorVariables.push(variableName);
return;
}
if (dryRun) {
return;
}
let stokables = node as MinimalStrokesMixin;
const strokessCopy = clone(stokables.strokes)
// Fills and strokes must be set via their immutable arrays
strokessCopy[0] = figma.variables.setBoundVariableForPaint(strokessCopy[0], 'color', color)
stokables.strokes = strokessCopy;
console.log('SETTING stroke color for: ', path, 'var name: ', variableName);
}
} catch (error) {
console.log('ASYNC error processing node: ', node.name, error);
throw error;
}
});
//console.log('done processing component: ', component.name);
} catch (e) {
console.log('error processing component: ', component.name, e);
}
}
notFountColorVariables.length && console.log('Could not find the following color variables', notFountColorVariables);
console.log('done');
return;
}
run().then(() => {
figma.closePlugin()
});
function clone(val: any) {
const type = typeof val
if (val === null) {
return null
} else if (type === 'undefined' || type === 'number' ||
type === 'string' || type === 'boolean') {
return val
} else if (type === 'object') {
if (val instanceof Array) {
return val.map(x => clone(x))
} else if (val instanceof Uint8Array) {
return new Uint8Array(val)
} else {
let o = {}
for (const key in val) {
o[key] = clone(val[key])
}
return o
}
}
throw 'unknown'
}
const fillableNodes = new Set(['RECTANGLE', 'ELLIPSE', 'POLYGON', 'STAR', 'LINE', 'VECTOR', 'TEXT', 'FRAME', 'COMPONENT', 'INSTANCE', 'BOOLEAN_OPERATION', 'SLICE', 'GROUP', 'BOOLEAN_OPERATION']);
async function traverse(component: SceneNode, path: string, callback: (node: SceneNode, path: string) => Promise<void>) {
await callback(component, path);
if ('children' in component) {
//console.log("traversing children:", path + '/..', component.children.length);
for (let i = 0; i < component.children.length; i++) {
const child = component.children[i];
if(child.type === 'INSTANCE') {
continue;
}
//console.log("traversing child: ", child.type, child.name);
await traverse(child, path + '/' + child.name, callback);
//console.log("after traverse child:", path + '/' + child.name);
}
}
}
function getVariableName(cssValue: string) {
if (!cssValue) {
return null;
}
let startIndex = cssValue.indexOf('var(');
if (startIndex === -1) {
return null;
}
let endIndex = cssValue.indexOf(',', startIndex);
return cssValue.substring(startIndex + 6, endIndex); //'var(--'
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment