-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(scripts): allow import script to pull using Figma API
- handle both downloaded file version and API version - handle input and edge cases (e.g., missing config) - separate and handle parsing of token contents into theme file - guard against committing local edsrc config
- Loading branch information
1 parent
717938f
commit dfec8fb
Showing
6 changed files
with
18,506 additions
and
3 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 |
---|---|---|
|
@@ -27,6 +27,7 @@ storybook-components | |
.env.production.local | ||
.cache | ||
.idea | ||
.edsrc.json | ||
|
||
npm-debug.log* | ||
yarn-debug.log* | ||
|
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
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,253 @@ | ||
#!/usr/bin/env node | ||
|
||
class AbstractFigmaVariableReader { | ||
constructor(jsonData) { | ||
this._jsonData = jsonData; | ||
} | ||
|
||
getModes(variableCollectionId) { | ||
throw new Error('Cannot Use Abstract class directly'); | ||
} | ||
|
||
getVariableCollections() { | ||
throw new Error('Cannot Use Abstract class directly'); | ||
} | ||
|
||
getVariablesByCollectionId(variableCollectionId) { | ||
throw new Error('Cannot Use Abstract class directly'); | ||
} | ||
|
||
getVariableValue(variableId, mode) { | ||
throw new Error('Cannot Use Abstract class directly'); | ||
} | ||
|
||
parseResolvedValue(varType, figmaResolvedValue) { | ||
if (varType === 'COLOR') { | ||
const r = Math.floor(figmaResolvedValue.r * 255); | ||
const g = Math.floor(figmaResolvedValue.g * 255); | ||
const b = Math.floor(figmaResolvedValue.b * 255); | ||
const a = figmaResolvedValue.a; | ||
if (figmaResolvedValue.a > 0 && figmaResolvedValue.a < 1) { | ||
return `rgba(${r}, ${g}, ${b}, ${a})`; | ||
} else { | ||
// print hex instead | ||
return ( | ||
'#' + | ||
[r, g, b] | ||
.map((x) => x.toString(16)) | ||
.map((x) => (x.length === 1 ? '0' + x : x)) | ||
.join('') | ||
.toUpperCase() | ||
); | ||
} | ||
} else { | ||
throw new TypeError('unknown resolved varType: ' + varType, { | ||
details: figmaResolvedValue, | ||
}); | ||
} | ||
} | ||
} | ||
|
||
// https://www.figma.com/developers/api#get-published-variables-endpoint | ||
class FigmaAPIReader extends AbstractFigmaVariableReader { | ||
constructor(jsonData) { | ||
super(jsonData.meta); | ||
} | ||
|
||
getModes(variableCollectionId) { | ||
return this._jsonData.variableCollections[variableCollectionId].modes; | ||
} | ||
|
||
getVariableCollections() { | ||
return Object.values(this._jsonData.variableCollections).map( | ||
(collection) => { | ||
return { | ||
id: collection.id, | ||
name: collection.name, | ||
}; | ||
}, | ||
); | ||
} | ||
|
||
getVariablesByCollectionId(variableCollectionId) { | ||
return Object.values(this._jsonData.variables).filter((variable) => { | ||
return variable.variableCollectionId === variableCollectionId; | ||
}); | ||
} | ||
} | ||
|
||
(async function () { | ||
const jsonfile = require('jsonfile'); | ||
const chalk = require('chalk'); | ||
const { prompt } = require('enquirer'); | ||
const set = require('lodash/set'); | ||
const { default: ora } = await import('ora'); | ||
|
||
// eslint-disable-next-line import/extensions | ||
const { hideBin } = require('yargs/helpers'); | ||
const yargs = require('yargs/yargs'); | ||
|
||
const { getConfig, parseResolvedValue, getWritePath } = require('./_util'); | ||
|
||
// Set up the command structure to output help if the file to load is not specified | ||
const args = yargs(hideBin(process.argv)) | ||
.command( | ||
'$0 file [file_id]', | ||
'Fetch the variables from Figma using the API', | ||
(yargs) => { | ||
yargs.positional('file_id', { | ||
type: 'string', | ||
describe: 'the figma file where the published variables are defined', | ||
}); | ||
}, | ||
) | ||
.option('token', { | ||
describe: 'Figma API token', | ||
type: 'string', | ||
}) | ||
.option('verbose', { | ||
describe: 'Print additional details for debugging purposes', | ||
type: 'boolean', | ||
}) | ||
.option('dry-run', { | ||
describe: 'Run the script without writing any changes', | ||
type: 'boolean', | ||
}).argv; | ||
|
||
// read in the config from config file, package json "eds", etc. | ||
const config = await getConfig(); | ||
const isVerbose = args.v; | ||
const canWrite = !args.dryRun; | ||
|
||
// Get the local variables from the defined file b/c published ones won't contain the modes | ||
// https://www.figma.com/developers/api#get-local-variables-endpoint | ||
const figmaFileResponse = await fetch( | ||
`https://api.figma.com/v1/files/${args.file_id}/variables/local`, | ||
{ | ||
headers: { | ||
'X-FIGMA-TOKEN': args.token, | ||
}, | ||
}, | ||
); | ||
|
||
if (!figmaFileResponse.ok) { | ||
throw new Error('cannot access figma file: ' + figmaFileResponse.status); | ||
} | ||
|
||
const fullData = await figmaFileResponse.json(); | ||
const figmaApiReader = new FigmaAPIReader(fullData); | ||
const edsCollection = figmaApiReader | ||
.getVariableCollections() | ||
.find((collection) => collection.name === 'EDS tokens'); | ||
|
||
// Determine which of the modes in the file should be used | ||
// TODO-AH: define naming convention for tier 1 collection and EDS token collection so that we can map | ||
// the two together. | ||
let response; | ||
try { | ||
response = await prompt({ | ||
name: 'modeId', | ||
message: 'Please select the Figma mode for token import', | ||
type: 'select', | ||
choices: figmaApiReader.getModes(edsCollection.id).map((mode) => { | ||
return { | ||
name: mode.modeId, | ||
message: `Use the ${mode.name} figma mode`, | ||
value: mode.name, | ||
}; | ||
}), | ||
}); | ||
} catch (e) { | ||
// e.g., someone hits ESC | ||
console.error(chalk.red('Aborted.'), e); | ||
process.exit(-1); | ||
} | ||
|
||
const stats = { | ||
skipped: [], | ||
updated: [], | ||
errored: [], | ||
total: [], | ||
}; | ||
|
||
const spinner = ora('Parsing tokens').start(); | ||
|
||
// now, load in the local theme file. We want to look up keys in there | ||
const localTheme = jsonfile.readFileSync(`${config.src}app-theme.json`); | ||
|
||
figmaApiReader | ||
.getVariablesByCollectionId(edsCollection.id) | ||
.forEach((figmaVariable) => { | ||
stats.total.push(figmaVariable.id); | ||
|
||
// TODO-AH: all the write paths are invalid | ||
const writePath = getWritePath( | ||
localTheme, | ||
'themes', // TODO-AH : hardcoded to only write semantic tokens b/c of naming convention | ||
figmaVariable.name, | ||
); | ||
console.log(writePath); | ||
|
||
if (writePath) { | ||
try { | ||
canWrite && | ||
set( | ||
localTheme, | ||
writePath, | ||
parseResolvedValue( | ||
figmaVariable.type, | ||
figmaVariable.resolvedValuesByMode[response.modeId] | ||
.resolvedValue, | ||
), | ||
); | ||
|
||
// write the value using the calculated path and parsed value | ||
if (isVerbose || !canWrite) { | ||
spinner.succeed( | ||
'Write: ' + | ||
parseResolvedValue( | ||
figmaVariable.type, | ||
figmaVariable.resolvedValuesByMode[response.modeId] | ||
.resolvedValue, | ||
) + | ||
' to ' + | ||
writePath, | ||
); | ||
} | ||
|
||
spinner.text = chalk.bold(figmaVariable.name) + ': Done!'; | ||
stats.updated.push(figmaVariable); | ||
} catch (e) { | ||
// We couldn't parse the resolved value, so skip and add to errors | ||
spinner.fail( | ||
chalk.bold(figmaVariable.name) + | ||
': Skipped with error (' + | ||
e.message + | ||
')', | ||
); | ||
stats.errored.push(figmaVariable); | ||
} | ||
} else { | ||
spinner.text = chalk.bold(figmaVariable.name) + ': Skipped'; | ||
|
||
if (!writePath) { | ||
spinner.warn( | ||
chalk.bold(figmaVariable.name) + | ||
': Skipped with warning (no write path)', | ||
); | ||
} | ||
stats.skipped.push(figmaVariable); | ||
} | ||
}); | ||
|
||
if (canWrite) { | ||
jsonfile.writeFileSync(`${config.src}app-theme.json`, localTheme, { | ||
spaces: 2, | ||
finalEOL: false, | ||
}); | ||
} | ||
|
||
spinner.succeed( | ||
`Done! updated: ${stats.updated.length}, skipped: ${stats.skipped.length}, errored: ${stats.errored.length}, total: ${stats.total.length}`, | ||
); | ||
})(); |
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
Oops, something went wrong.