-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
maintenance: implement
@packages/figma-to-css-variables
- Loading branch information
Showing
9 changed files
with
628 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
tmp |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
# Figma to CSS Variables | ||
|
||
## Overview | ||
|
||
This script is designed to handle various tasks related to Figma local variables and Style Dictionary. It can fetch Figma local variables, transform them for Style Dictionary, and run Style Dictionary with specified options. | ||
|
||
## Environment Variables | ||
|
||
The script requires the following environment variables: | ||
|
||
- `FIGMA_FILE_KEY`: The key of the Figma file. | ||
- `FIGMA_ACCESS_TOKEN`: The token to access the Figma API. | ||
|
||
## Command Line Arguments | ||
|
||
The script accepts the following command line arguments: | ||
|
||
- `--fetch`: Fetches Figma local variables. | ||
- `--transform`: Transforms variables for Style Dictionary. | ||
- `--generate`: Runs Style Dictionary with the specified output path. | ||
- `--output <path>`: Specifies the output path for the generated files. Defaults to 'build/css'. | ||
- `--filter-modes <modes>`: Specifies filter modes for Style Dictionary as a comma-separated list. | ||
|
||
If no specific argument is provided, the script defaults to running all steps in sequence. | ||
|
||
## Usage | ||
|
||
To run the script, use the following command: | ||
|
||
```sh | ||
FIGMA_FILE_KEY=FnK... FIGMA_ACCESS_TOKEN=figd_xxx pnpm --filter @packages/figma-to-css-variables gen --output '../../apps/service-site/src/styles' --filter-modes 'Dark,Mode 1' | ||
``` |
36 changes: 36 additions & 0 deletions
36
frontend/packages/figma-to-css-variables/bin/fetchFigmaLocalVariables.mjs
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { promises as fs } from 'node:fs' | ||
import { mkdir } from 'node:fs/promises' | ||
|
||
/** | ||
* Fetches local variables from the Figma API and saves them to a temporary file. | ||
* @returns {Promise<void>} | ||
*/ | ||
export async function fetchFigmaLocalVariables() { | ||
const FIGMA_FILE_KEY = process.env.FIGMA_FILE_KEY | ||
const FIGMA_ACCESS_TOKEN = process.env.FIGMA_ACCESS_TOKEN | ||
|
||
if (!FIGMA_FILE_KEY || !FIGMA_ACCESS_TOKEN) { | ||
throw new Error( | ||
'FIGMA_FILE_KEY and FIGMA_ACCESS_TOKEN environment variables are required.', | ||
) | ||
} | ||
|
||
const url = `https://api.figma.com/v1/files/${FIGMA_FILE_KEY}/variables/local` | ||
const headers = { | ||
'X-Figma-Token': FIGMA_ACCESS_TOKEN, | ||
} | ||
|
||
const response = await fetch(url, { headers }) | ||
if (!response.ok) { | ||
throw new Error(`Failed to fetch variables: ${response.statusText}`) | ||
} | ||
|
||
const data = await response.json() | ||
|
||
await mkdir('tmp', { recursive: true }) | ||
await fs.writeFile( | ||
'tmp/local-variables.json', | ||
JSON.stringify(data.meta, null, 2), | ||
) | ||
console.info('Local variables fetched and saved to tmp/local-variables.json') | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import { fetchFigmaLocalVariables } from './fetchFigmaLocalVariables.mjs' | ||
import { runStyleDictionary } from './runStyleDictionary.mjs' | ||
import { transformVariablesForStyleDictionary } from './transformVariablesForStyleDictionary.mjs' | ||
|
||
/** | ||
* Main function to handle command line arguments and execute corresponding tasks. | ||
* | ||
* Command line arguments: | ||
* - `--fetch`: Fetches Figma local variables. | ||
* - `--transform`: Transforms variables for Style Dictionary. | ||
* - `--generate`: Runs Style Dictionary with the specified output path. | ||
* - `--output <path>`: Specifies the output path for the generated files. Defaults to 'build/css'. | ||
* - `--filter-modes <modes>`: Specifies filter modes for Style Dictionary as a comma-separated list. | ||
* | ||
* If no specific argument is provided, it defaults to running all steps in sequence. | ||
* | ||
* @returns {Promise<void>} A promise that resolves when all tasks are completed. | ||
*/ | ||
async function main() { | ||
const args = process.argv.slice(2) | ||
|
||
let outputPath = null | ||
const filterModes = [] | ||
|
||
if (args.includes('--output')) { | ||
const outputIndex = args.indexOf('--output') | ||
if (outputIndex !== -1 && args[outputIndex + 1]) { | ||
outputPath = args[outputIndex + 1] | ||
} | ||
} | ||
|
||
if (args.includes('--filter-modes')) { | ||
const filterModesIndex = args.indexOf('--filter-modes') | ||
if (filterModesIndex !== -1 && args[filterModesIndex + 1]) { | ||
filterModes.push(...args[filterModesIndex + 1].split(',')) | ||
} | ||
} | ||
|
||
if (args.includes('--fetch')) { | ||
await fetchFigmaLocalVariables() | ||
} else if (args.includes('--transform')) { | ||
await transformVariablesForStyleDictionary() | ||
} else if (args.includes('--generate')) { | ||
await runStyleDictionary(outputPath, filterModes) | ||
} else { | ||
// Default to running all steps | ||
await fetchFigmaLocalVariables() | ||
await transformVariablesForStyleDictionary() | ||
await runStyleDictionary(outputPath, filterModes) | ||
} | ||
} | ||
|
||
main().catch((err) => { | ||
console.error('Error:', err) | ||
}) |
109 changes: 109 additions & 0 deletions
109
frontend/packages/figma-to-css-variables/bin/runStyleDictionary.mjs
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import fs from 'node:fs' | ||
import path from 'node:path' | ||
import StyleDictionary from 'style-dictionary' | ||
|
||
function isNumber(token) { | ||
const targets = [ | ||
'size', | ||
'border-radius', | ||
'border-width', | ||
'spacing', | ||
'font-size', | ||
'line-height', | ||
'sizing', | ||
] | ||
|
||
return ( | ||
targets.includes(token.attributes?.category ?? '') || | ||
targets.includes(token.attributes?.type ?? '') | ||
) | ||
} | ||
|
||
StyleDictionary.registerTransform({ | ||
type: 'value', | ||
transitive: true, | ||
name: 'number/px', | ||
filter: isNumber, | ||
transform: (token) => { | ||
const val = Number.parseFloat(token.value) | ||
if (Number.isNaN(val)) | ||
throw `Invalid Number: '${token.name}: ${token.value}' is not a valid number, cannot transform to 'px' \n` | ||
return `${val}px` | ||
}, | ||
}) | ||
|
||
function source(input) { | ||
return [`${input}/**/*.json`] | ||
} | ||
|
||
/** | ||
* Runs the Style Dictionary build process for each sub-directory found in the input directory. | ||
* | ||
* @param {string[]} filterDirNames - An array of directory names to filter and process. | ||
* @param {string} [outputPath='build/css'] - The output path where the CSS files will be generated. | ||
* @returns {Promise<void>} - A promise that resolves when the build process is complete. | ||
*/ | ||
export async function runStyleDictionary( | ||
outputPath = 'build/css', | ||
filterDirNames = [], | ||
) { | ||
try { | ||
const inputDir = path.resolve('tmp/transformed-variables') | ||
const subDirs = fs | ||
.readdirSync(inputDir, { withFileTypes: true }) | ||
.filter((dirent) => dirent.isDirectory()) | ||
.filter( | ||
(dirent) => | ||
filterDirNames.length === 0 || filterDirNames.includes(dirent.name), | ||
) | ||
.map((dirent) => dirent.name) | ||
|
||
for (const subDir of subDirs) { | ||
const subDirInput = path.join(inputDir, subDir) | ||
const subDirOutput = `${path.join(outputPath, subDir)}/` | ||
|
||
const config = { | ||
source: [source(subDirInput)], | ||
platforms: { | ||
css: { | ||
transformGroup: 'css', | ||
buildPath: subDirOutput, | ||
files: [ | ||
{ | ||
destination: 'variables.css', | ||
format: 'css/variables', | ||
}, | ||
], | ||
/** | ||
* Resetting the configuration to display size-related numbers in px. | ||
* The default settings are as follows: | ||
* @see: https://styledictionary.com/reference/hooks/transform-groups/predefined/ | ||
*/ | ||
transforms: [ | ||
'attribute/cti', | ||
'name/kebab', | ||
'time/seconds', | ||
'html/icon', | ||
'color/css', | ||
'number/px', | ||
], | ||
}, | ||
}, | ||
log: { | ||
warnings: 'warn', // 'warn' | 'error' | 'disabled' | ||
verbosity: 'verbose', // 'default' | 'silent' | 'verbose' | ||
errors: { | ||
brokenReferences: 'console', // 'throw' | 'console' | ||
}, | ||
}, | ||
} | ||
|
||
const styleDictionary = new StyleDictionary(config) | ||
await styleDictionary.buildAllPlatforms() | ||
} | ||
|
||
console.info('Style Dictionary build complete.') | ||
} catch (error) { | ||
console.error(`Error during Style Dictionary generation: ${error.message}`) | ||
} | ||
} |
126 changes: 126 additions & 0 deletions
126
frontend/packages/figma-to-css-variables/bin/transformVariablesForStyleDictionary.mjs
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import { promises as fs } from 'node:fs' | ||
import { mkdir } from 'node:fs/promises' | ||
import path from 'node:path' | ||
|
||
/** | ||
* Retrieves the token value from the variable based on the mode. | ||
* @param {Object} variable | ||
* @param {string} modeId | ||
* @param {Object[]} localVariables | ||
* @returns {string|number} | ||
*/ | ||
function tokenValueFromVariable(variable, modeId, localVariables) { | ||
const value = variable.valuesByMode[modeId] | ||
if (typeof value === 'object') { | ||
if ('type' in value && value.type === 'VARIABLE_ALIAS') { | ||
const aliasedVariable = localVariables[value.id] | ||
// console.log(aliasedVariable, value.id) | ||
return `{${aliasedVariable?.name.replace(/\//g, '.')}}` | ||
} | ||
if ('r' in value) { | ||
return rgbToHex(value) // Assuming rgbToHex is defined elsewhere | ||
} | ||
|
||
throw new Error(`Format of variable value is invalid: ${value}`) | ||
} | ||
|
||
if (typeof value === 'number') { | ||
return Number.isInteger(value) ? value : Math.floor(value * 100) / 100 | ||
} | ||
|
||
return value | ||
} | ||
|
||
function rgbToHex({ r, g, b, a }) { | ||
if (a === undefined) { | ||
a = 1 | ||
} | ||
|
||
const toHex = (value) => { | ||
const hex = Math.round(value * 255).toString(16) | ||
return hex.length === 1 ? `0${hex}` : hex | ||
} | ||
|
||
const hex = [toHex(r), toHex(g), toHex(b)].join('') | ||
return `#${hex}${a !== 1 ? toHex(a) : ''}` | ||
} | ||
|
||
/** | ||
* Infers token type from the variable. | ||
* @param {Object} variable | ||
* @returns {string} | ||
*/ | ||
function tokenTypeFromVariable(variable) { | ||
switch (variable.resolvedType) { | ||
case 'BOOLEAN': | ||
return 'boolean' | ||
case 'COLOR': | ||
return 'color' | ||
case 'FLOAT': | ||
return 'number' | ||
case 'STRING': | ||
return 'string' | ||
} | ||
} | ||
|
||
/** | ||
* Transforms Figma local variables into Style Dictionary format. | ||
* @returns {Promise<void>} | ||
*/ | ||
export async function transformVariablesForStyleDictionary() { | ||
const localVariablesPath = 'tmp/local-variables.json' | ||
const transformedDir = 'tmp/transformed-variables' | ||
|
||
const data = JSON.parse(await fs.readFile(localVariablesPath, 'utf-8')) | ||
const localVariables = data.variables | ||
const localVariableCollections = data.variableCollections | ||
|
||
await mkdir(transformedDir, { recursive: true }) | ||
|
||
const fileContents = {} | ||
|
||
for (const variable of Object.values(localVariables)) { | ||
if (variable.remote) continue // Skip remote variables | ||
|
||
const collection = localVariableCollections[variable.variableCollectionId] | ||
if (collection) { | ||
for (const mode of collection.modes) { | ||
const fileName = `${mode.name}/${collection.name}.json` | ||
const filePath = path.join(transformedDir, fileName) | ||
|
||
if (!fileContents[filePath]) { | ||
fileContents[filePath] = {} | ||
} | ||
|
||
let obj = fileContents[filePath] | ||
for (const groupName of variable.name.split('/')) { | ||
obj[groupName] = obj[groupName] || {} | ||
obj = obj[groupName] | ||
} | ||
|
||
const token = { | ||
type: tokenTypeFromVariable(variable), | ||
value: tokenValueFromVariable(variable, mode.modeId, localVariables), | ||
description: variable.description || '', | ||
extensions: { | ||
'com.figma': { | ||
hiddenFromPublishing: variable.hiddenFromPublishing, | ||
scopes: variable.scopes, | ||
codeSyntax: variable.codeSyntax, | ||
}, | ||
}, | ||
} | ||
|
||
Object.assign(obj, token) | ||
} | ||
} | ||
} | ||
|
||
for (const [filePath, content] of Object.entries(fileContents)) { | ||
const dir = path.dirname(filePath) | ||
await mkdir(dir, { recursive: true }) | ||
await fs.writeFile(filePath, JSON.stringify(content, null, 2)) | ||
} | ||
|
||
console.info('Variables transformed and saved to tmp/transformed-variables/') | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"extends": ["../../packages/configs/biome.jsonc"] | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
{ | ||
"name": "@packages/figma-to-css-variables", | ||
"main": "bin/index.mjs", | ||
"scripts": { | ||
"gen": "node bin/index.mjs", | ||
"lint": "conc -c auto pnpm:lint:*", | ||
"lint:biome": "biome check .", | ||
"fmt": "conc -c auto pnpm:fmt:*", | ||
"fmt:biome": "biome check --write --unsafe ." | ||
}, | ||
"devDependencies": { | ||
"@biomejs/biome": "1.9.3", | ||
"@packages/configs": "workspace:*" | ||
}, | ||
"dependencies": { | ||
"style-dictionary": "^4.1.3" | ||
} | ||
} |
Oops, something went wrong.