Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Figma tokens build process #665

Merged
merged 2 commits into from
Jul 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sixty-teachers-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/primitives': minor
---

Adding support for figma tokens
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,32 @@ A `mix` proprty must always have a `color` and a `weight` child. `color` can be
}
```

#### Extensions property

According to the [w3c design token specs](https://design-tokens.github.io/community-group/format/#design-token), the [`$extensions`](https://design-tokens.github.io/community-group/format/#extensions) property is used for additional meta data.

For our Figma export we use the following meta data:

- `collection` the collection that the token is added to within Figma
- `mode` the mode that the token is added to within the collection in Figma
- `scopes` the scopes that are assigned to the token in Figma, the actual Figma compatible `scopes` are retreive from an object in the [figmaAttributes transformer](./src/transformers/figmaAttributes.ts)

Code example

```js
bgColor: {
$value: '{borderColor.accent.muted}',
$type: 'color',
$extensions: {
'org.primer.figma': {
collection: 'pattern/mode',
mode: 'light',
scopes: ['bgColor'],
},
},
}
```

## License

[MIT](./LICENSE) © [GitHub](https://github.com/)
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"build": "ts-node ./scripts/build.ts && tsc --project tsconfig.build.json",
"build:next": "ts-node -r tsconfig-paths/register ./scripts/buildTokens.ts && ts-node ./scripts/buildFallbacks.ts",
"build:tokens": "node -e \"require('./build')._init()\"",
"build:new-tokens": "npm run build:tokens && npm run build:next",
"build:figma": "ts-node scripts/buildPlatforms/buildFigma.ts",
"build:new-tokens": "npm run build:tokens && npm run build:next && npm run build:figma",
"tokenJson:check": "ts-node scripts/diffThemes.ts && ts-node scripts/diffTokenProps.ts",
"contrast:check": "ts-node -e \"require('./scripts/color-contrast').check()\"",
"format": "prettier --write '**/*.{js,jsx,ts,tsx,md,mdx,css}'",
Expand Down
111 changes: 111 additions & 0 deletions scripts/buildPlatforms/buildFigma.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import fs from 'fs'
import {PrimerStyleDictionary} from '~/src/PrimerStyleDictionary'
import {themes} from '../themes.config'
import {figma} from '~/src/platforms'
import type {ConfigGeneratorOptions} from '~/src/types/StyleDictionaryConfigGenerator'

export const buildFigma = (buildOptions: ConfigGeneratorOptions): void => {
/** -----------------------------------
* Colors
* ----------------------------------- */
// base colors
const baseScales = [
{
name: 'light',
source: [`src/tokens/base/color/light/light.json5`],
},
{
name: 'dark',
source: [`src/tokens/base/color/dark/dark.json5`],
},
// {
// name: 'dark-dimmed',
// source: [`src/tokens/base/color/dark/dark.json5`, `src/tokens/base/color/dark/dark.dimmed.json5`],
// },
]

for (const {name, source} of baseScales) {
PrimerStyleDictionary.extend({
source,
platforms: {
figma: figma(`figma/scales/${name}.json`, buildOptions.prefix, buildOptions.buildPath),
},
}).buildAllPlatforms()
}
//
for (const {filename, source, include} of themes) {
if (['light', 'dark' /*, 'dark-dimmed'*/].includes(filename)) {
// build functional scales
PrimerStyleDictionary.extend({
source,
include,
platforms: {
figma: figma(`figma/themes/${filename}.json`, buildOptions.prefix, buildOptions.buildPath, {
mode: filename,
}),
},
}).buildAllPlatforms()
}
}
/** -----------------------------------
* Size tokens
* ----------------------------------- */
const sizeFiles = [
'src/tokens/base/size/size.json',
'src/tokens/functional/size/breakpoints.json',
'src/tokens/functional/size/size.json',
'src/tokens/functional/size/border.json',
// 'src/tokens/functional/size/size-fine.json',
// 'src/tokens/functional/size/size-coarse.json',
]
//
PrimerStyleDictionary.extend({
source: sizeFiles,
include: sizeFiles,
platforms: {
figma: figma(`figma/dimension/dimension.json`, buildOptions.prefix, buildOptions.buildPath),
},
}).buildAllPlatforms()

/** -----------------------------------
* Create list of files
* ----------------------------------- */
const dirNames = fs
.readdirSync(`${buildOptions.buildPath}figma`, {withFileTypes: true})
.filter(dir => dir.isDirectory())
.map(dir => dir.name)

const files = dirNames.flatMap(dir => {
const localFiles = fs.readdirSync(`${buildOptions.buildPath}figma/${dir}`)
return localFiles.map(file => `${buildOptions.buildPath}figma/${dir}/${file}`)
})

const tokens: {
collection: string
mode: string
}[] = files.flatMap(filePath => JSON.parse(fs.readFileSync(filePath, 'utf8')))
const collections: Record<string, string[]> = {}

for (const {collection, mode} of tokens) {
if (!(collection in collections)) {
collections[collection] = []
}
if (!collections[collection].includes(mode)) {
collections[collection].push(mode)
}
}

// define the order of the modes
// we inverse it to deal with the -1 of the indexOf if item is not found in the array: basically anything that is not in the list should come last
const modeOrder = ['light', 'dark'].reverse()
// sort modes in the order defined above
for (const collection in collections) {
collections[collection].sort((a, b) => modeOrder.indexOf(b) - modeOrder.indexOf(a))
}
// write to file
fs.writeFileSync(`${buildOptions.buildPath}figma/figma.json`, JSON.stringify({collections, files}, null, 2))
}

buildFigma({
buildPath: 'tokens-next-private/',
})
2 changes: 2 additions & 0 deletions scripts/buildTokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {ConfigGeneratorOptions, StyleDictionaryConfigGenerator} from '~/src
import type {TokenBuildInput} from '~/src/types/TokenBuildInput'
import glob from 'fast-glob'
import {themes} from './themes.config'
import {buildFigma} from './buildPlatforms/buildFigma'
/**
* getStyleDictionaryConfig
* @param filename output file name without extension
Expand Down Expand Up @@ -46,6 +47,7 @@ const getStyleDictionaryConfig: StyleDictionaryConfigGenerator = (
})

export const buildDesignTokens = (buildOptions: ConfigGeneratorOptions): void => {
buildFigma(buildOptions)
/** -----------------------------------
* Colors, shadows & borders
* ----------------------------------- */
Expand Down
6 changes: 6 additions & 0 deletions src/PrimerStyleDictionary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
jsonPostCssFallback,
cssWrapMediaQuery,
cssVariables,
jsonFigma,
} from './formats'

/**
Expand Down Expand Up @@ -99,6 +100,11 @@ StyleDictionary.registerFormat({
formatter: jsonPostCssFallback,
})

StyleDictionary.registerFormat({
name: 'json/figma',
formatter: jsonFigma,
})

/**
* Transformers
*
Expand Down
1 change: 1 addition & 0 deletions src/formats/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export {cssThemed} from './cssThemed'
export {cssCustomMedia} from './cssCustomMedia'
export {cssWrapMediaQuery} from './cssWrapMediaQuery'
export {cssVariables} from './cssVariables'
export {jsonFigma} from './jsonFigma'
export {javascriptCommonJs} from './javascriptCommonJs'
export {javascriptEsm} from './javascriptEsm'
export {jsonNestedPrefixed} from './jsonNestedPrefixed'
Expand Down
62 changes: 62 additions & 0 deletions src/formats/jsonFigma.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import StyleDictionary from 'style-dictionary'
import {format} from 'prettier'
import type {FormatterArguments} from 'style-dictionary/types/Format'
const {sortByReference} = StyleDictionary.formatHelpers

const replaceRegEx = /(?:{|})/g

const isReference = (string: string): boolean => /^\{([^\\]*)\}$/g.test(string)

const getReference = (dictionary: StyleDictionary.Dictionary, refString: string) => {
if (isReference(refString)) {
// retrieve reference token
const refToken = dictionary.getReferences(refString)[0]
// return full reference
return [refToken.attributes?.collection, ...refToken.path].filter(Boolean).join('/')
}
return undefined
}

const getFigmaType = (type: string): string => {
const validTypes = {
color: 'COLOR',
dimension: 'FLOAT',
}
if (type in validTypes) return validTypes[type as keyof typeof validTypes]
throw new Error(`Invalid type: ${type}`)
}

/**
* @description Converts `StyleDictionary.dictionary.tokens` to javascript esm (javascript export statement)
* @param arguments [FormatterArguments](https://github.com/amzn/style-dictionary/blob/main/types/Format.d.ts)
* @returns formatted `string`
*/
export const jsonFigma: StyleDictionary.Formatter = ({
dictionary,
file: _file,
platform: _platform,
}: FormatterArguments) => {
// sort tokens by reference
const tokens = dictionary.allTokens.sort(sortByReference(dictionary)).map(token => {
const {attributes, value, $type, comment: description, original, alpha, mix} = token
const {mode, collection, scopes} = attributes || {}
const tokenName = token.name.replace(replaceRegEx, '')
return {
name: tokenName,
value,
type: getFigmaType($type),
alpha,
isMix: mix ? true : undefined,
description,
refId: [collection, tokenName].filter(Boolean).join('/'),
reference: getReference(dictionary, original.value),
collection,
mode,
scopes,
}
})
// add file header and convert output
const output = JSON.stringify(tokens, null, 2)
// return prettified
return format(output, {parser: 'json', printWidth: 500})
}
46 changes: 46 additions & 0 deletions src/platforms/figma.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type StyleDictionary from 'style-dictionary'
import type {PlatformInitializer} from '~/src/types/PlatformInitializer'
import {isSource} from '~/src/filters'

const validFigmaToken = (token: StyleDictionary.TransformedToken) => {
const validTypes = ['color', 'dimension']
// is a siource token, not an included one
if (!isSource(token)) return false
// has a collection attribute
if (
!('$extensions' in token) ||
!('org.primer.figma' in token.$extensions) ||
!('collection' in token.$extensions['org.primer.figma'])
)
return false
// is a color or dimension type
return validTypes.includes(token.$type)
}

export const figma: PlatformInitializer = (outputFile, prefix, buildPath, options): StyleDictionary.Platform => ({
prefix,
buildPath,
transforms: [
'color/rgbaFloat',
'name/pathToSlashNotation',
'figma/attributes',
'dimension/pixelUnitless',
// 'border/figma',
// 'typography/figma',
'fontWeight/number',
],
options: {
basePxFontSize: 16,
...options,
},
files: [
{
destination: outputFile,
filter: validFigmaToken,
format: `json/figma`,
options: {
outputReferences: true,
},
},
],
})
1 change: 1 addition & 0 deletions src/platforms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export {css} from './css'
export {deprecatedJson} from './deprecatedJson'
export {docJson} from './docJson'
export {fallbacks} from './fallbacks'
export {figma} from './figma'
export {javascript} from './javascript'
export {json} from './json'
export {scss} from './scss'
Expand Down
Loading