-
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(script): add import from figma script (#1844)
* feat(script): add import from figma script - read in the output from the Export/Import Variables file - allow user to select which mode to read from - update the local theme file with the values - allow for multi-modal function (use internal and external) - DX improvements for this CLI of the script via new lib.s - address PR comments, factoring, and linting issues
- Loading branch information
1 parent
a93ae85
commit 9ed90e5
Showing
9 changed files
with
487 additions
and
26 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
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 |
---|---|---|
|
@@ -140,4 +140,114 @@ module.exports = { | |
} | ||
} | ||
}, | ||
/** | ||
* Determine the write path by taking the collection and variable name, and looking it up in | ||
* the existing local theme. If there's a path in the local theme file, we can write there (using lodash/set | ||
* or similar). | ||
* | ||
* @param {object} localTheme JSON file loaded, representing the data for the local theme | ||
* @param {string} collectionName Name of the exported collection | ||
* @param {string} variableName current variable name from figma (e.g., color/text/neutral/default) | ||
* @returns {string|null} representation of the path to write to in the local theme JSON file | ||
*/ | ||
getWritePath: function (localTheme, collectionName, variableName) { | ||
const at = require('lodash/at'); | ||
|
||
const workingPath = | ||
module.exports.getTokenPrefix(collectionName) + | ||
module.exports.tokenNameToPath(variableName); | ||
|
||
const found = at(localTheme, workingPath).filter( | ||
(entries) => typeof entries !== 'undefined', | ||
); | ||
|
||
if (found.length) { | ||
// handle case where we should look for @ in the file, then pluck the value object properly | ||
if (found[0]['@']?.value) { | ||
// update the write path to mark the @ and value | ||
return workingPath + '[email protected]'; | ||
} | ||
|
||
// handle case where it's just value | ||
if (found[0]?.value) { | ||
// update the write path to mark the value | ||
return workingPath + '.value'; | ||
} | ||
} | ||
|
||
// There is no write path based on what's in the local theme so we return null signal it's a missing token | ||
return null; | ||
}, | ||
/** | ||
* Utilities for handling parsing of Figma Theme Data. | ||
* | ||
* These functions are set up to handle, transform, and read data coming from figma API Structure. | ||
* For more information on this API format, refer to: | ||
* - https://help.figma.com/hc/en-us/articles/15339657135383-Guide-to-variables-in-Figma | ||
* - https://www.figma.com/plugin-docs/api/VariableCollection/ | ||
*/ | ||
/** | ||
* Given the value sourced from figma, translate it into a | ||
* style-dictionary format. When encountering an unrecognized type, | ||
* throw an error. | ||
* | ||
* Data Types: | ||
* - Type COLOR: https://www.figma.com/plugin-docs/api/RGB/ | ||
* | ||
* @param {string} type Figma type for the token value (Set:) | ||
* @param {object} figmaResolvedValue | ||
* @returns {string} value using the type | ||
* @throws {TypeError} using `details` on the data read from figma | ||
*/ | ||
parseResolvedValue: function (type, figmaResolvedValue) { | ||
if (type === '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 type: ' + type, { | ||
details: figmaResolvedValue, | ||
}); | ||
} | ||
}, | ||
/** | ||
* Given the "type" of import file (named after the collection name), produce | ||
* a prefix to the token name that corresponds to the prefix used for those | ||
* tokens. | ||
* | ||
* @param {string} collectionName The key to write to | ||
* @returns {string|null} a text prefix for where to write the token value or null when no prefix is found | ||
*/ | ||
getTokenPrefix: function (collectionName) { | ||
switch (collectionName) { | ||
case 'themes': | ||
return 'eds.theme.'; | ||
case 'primitives': | ||
return 'eds.'; | ||
default: | ||
return null; | ||
} | ||
}, | ||
/** | ||
* Conversion of the figma token name (e.g., some/path/to/token) to the equivalent path in a JSON object | ||
* @param {string} figmaTokenName The name from the figma variables panel (slash separated) | ||
* @returns {string} a lodash-compatible string representing the path to the token value in JSON | ||
*/ | ||
tokenNameToPath: function (figmaTokenName) { | ||
return figmaTokenName.replaceAll('/', '.').toLowerCase(); | ||
}, | ||
}; |
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,130 @@ | ||
const utils = require('./_util'); | ||
|
||
describe('utils', function () { | ||
describe('getWritePath', function () { | ||
const exampleTheme = { | ||
eds: { | ||
theme: { | ||
color: { | ||
body: { | ||
background: { | ||
neutral: { | ||
default: { | ||
'@': { | ||
value: '#FFFFFF', | ||
}, | ||
hover: { | ||
value: '#F4F6F8', | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}; | ||
|
||
it('generates a write path for a root-at value', () => { | ||
expect( | ||
utils.getWritePath( | ||
exampleTheme, | ||
'themes', | ||
'color/body/background/neutral/default', | ||
), | ||
).toEqual('[email protected]'); | ||
}); | ||
|
||
it('generates a write path for a non-root-at value', () => { | ||
expect( | ||
utils.getWritePath( | ||
exampleTheme, | ||
'themes', | ||
'color/body/background/neutral/default/hover', | ||
), | ||
).toEqual('eds.theme.color.body.background.neutral.default.hover.value'); | ||
}); | ||
|
||
it('generates a null when the path is not in the local theme', () => { | ||
expect( | ||
utils.getWritePath( | ||
exampleTheme, | ||
'themes', | ||
'color/body/background/neutral/default/focus', | ||
), | ||
).toEqual(null); | ||
}); | ||
}); | ||
|
||
describe('getTokenPath', function () { | ||
it.each([ | ||
['themes', 'eds.theme.'], | ||
['primitives', 'eds.'], | ||
['some-unknown', null], | ||
['', null], | ||
])( | ||
'parses the collection %s to token prefix "%s"', | ||
(collection, expected) => { | ||
expect(utils.getTokenPrefix(collection)).toEqual(expected); | ||
}, | ||
); | ||
}); | ||
|
||
describe('tokenNameToPath', function () { | ||
it('properly converts a token name to a lodash-compatible path', function () { | ||
expect(utils.tokenNameToPath('some/path/to/token')).toEqual( | ||
'some.path.to.token', | ||
); | ||
}); | ||
}); | ||
|
||
describe('parseResolvedValue', function () { | ||
const space500 = { | ||
r: 0.12941177189350128, | ||
g: 0.1568627506494522, | ||
b: 0.4000000059604645, | ||
a: 1, | ||
}; | ||
|
||
const blueprint300 = { | ||
r: 0.027450980618596077, | ||
g: 0.30588236451148987, | ||
b: 0.9098039269447327, | ||
a: 1, | ||
}; | ||
|
||
const neutral800 = { | ||
r: 0.12941177189350128, | ||
g: 0.15294118225574493, | ||
b: 0.1764705926179886, | ||
a: 1, | ||
}; | ||
|
||
const backgroundVeil = { | ||
r: 0.12941177189350128, | ||
g: 0.15294118225574493, | ||
b: 0.1764705926179886, | ||
a: 0.5, | ||
}; | ||
|
||
it.each` | ||
figmaColor | type | expected | ||
${space500} | ${'COLOR'} | ${'#212866'} | ||
${blueprint300} | ${'COLOR'} | ${'#074EE8'} | ||
${neutral800} | ${'COLOR'} | ${'#21272D'} | ||
${backgroundVeil} | ${'COLOR'} | ${'rgba(33, 39, 45, 0.5)'} | ||
`( | ||
'will use type $type to convert $figmaColor to $expected', | ||
({ figmaColor, type, expected }) => { | ||
expect(utils.parseResolvedValue(type, figmaColor)).toEqual(expected); | ||
}, | ||
); | ||
|
||
it('will throw on unrecognized types', () => { | ||
const test = () => { | ||
utils.parseResolvedValue('FLOAT', 0.5); | ||
}; | ||
expect(test).toThrow(TypeError); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.