Created
July 25, 2024 17:00
-
-
Save jurijsk/14d92b8f612ed4364389be6b82d244ec to your computer and use it in GitHub Desktop.
Figma Plugin Styles to Local Variables swap
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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