-
Notifications
You must be signed in to change notification settings - Fork 11
feat: Add DTCG tokens synced from Figma #350
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
base: main
Are you sure you want to change the base?
Changes from 12 commits
4797214
16d2d8a
3a67050
c5b3a0b
38a74d8
7398253
d5f187a
9d3bd0e
8bdc3af
1e7e3a2
64f8f8c
e079bd9
7b01ad7
38a3d4a
34c5fc3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| name: Tokens Sync automation | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| inputs: | ||
| toBranch: | ||
| description: 'Branch to sync tokens to' | ||
| required: true | ||
| default: main | ||
| type: choice | ||
| options: | ||
| - main | ||
| - prerelease/minor | ||
| - prerelease/major | ||
|
|
||
| jobs: | ||
| sync-tokens: | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| packages: write | ||
| pull-requests: write | ||
| contents: write | ||
| env: | ||
| NODE_VERSION: 24.x | ||
|
|
||
| steps: | ||
| - name: Set target branch name | ||
| id: target-branch | ||
| run: echo "name=sync-tokens-from-figma-${{ inputs.toBranch }}" >> $GITHUB_OUTPUT | ||
|
|
||
| - name: Checkout code | ||
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | ||
| with: | ||
| ref: ${{ inputs.toBranch }} | ||
|
|
||
| - name: Set global user | ||
| run: | | ||
| git config --global user.name "${{ github.actor }}" | ||
| git config --global user.email "${{ github.actor }}@users.noreply.github.com" | ||
|
|
||
| - name: Create branch | ||
| run: | | ||
| git checkout -b ${{ steps.target-branch.outputs.name }} | ||
|
|
||
| - uses: actions/setup-node@v7.0.0 | ||
| with: | ||
| node-version: ${{ env.NODE_VERSION }} | ||
|
|
||
|
RayRedGoose marked this conversation as resolved.
|
||
| - name: Cache node modules | ||
| id: yarn-cache | ||
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # 6.1.0 | ||
| with: | ||
| path: node_modules | ||
| key: ${{ runner.os }}-${{ env.NODE_VERSION }}-node-modules-hash-${{ hashFiles('yarn.lock') }} | ||
|
|
||
| - name: Install dev dependencies | ||
| if: steps.yarn-cache.outputs.cache-hit != 'true' | ||
| run: yarn install --production=false | ||
|
|
||
| - name: Fetch Figma variables | ||
| run: yarn fetch-tokens | ||
| env: | ||
| FIGMA_ACCESS_TOKEN: ${{ secrets.FIGMA_ACCESS_TOKEN }} | ||
| FIGMA_BASE_FILE_KEY: ${{ secrets.FIGMA_BASE_FILE_KEY }} | ||
| FIGMA_MAIN_FILE_KEY: ${{ secrets.FIGMA_MAIN_FILE_KEY }} | ||
|
|
||
| - name: Parse Figma variables | ||
| run: yarn parse-tokens | ||
|
|
||
| - name: Clear raw tokens | ||
| run: rm -rf figma-raw-tokens | ||
|
|
||
| - name: Check for token changes | ||
| id: changes | ||
| run: | | ||
| git add packages/canvas-tokens/dtcg/tokens | ||
| if git diff --cached --quiet; then | ||
| echo "No token changes found." | ||
| echo "has_changes=false" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "Token changes found." | ||
| echo "has_changes=true" >> "$GITHUB_OUTPUT" | ||
| fi | ||
|
|
||
| - name: Commit changes | ||
| if: steps.changes.outputs.has_changes == 'true' | ||
| run: | | ||
| git commit -m "feat: Sync tokens from Figma" | ||
| git push origin ${{ steps.target-branch.outputs.name }} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How do we want to handle the case of this workflow being dispatched when a previous sync PR is still open - would we use |
||
|
|
||
| - name: Create pull request | ||
| if: steps.changes.outputs.has_changes == 'true' | ||
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | ||
| with: | ||
| github-token: ${{ secrets.GH_RW_TOKEN }} | ||
| script: | | ||
| github.rest.pulls.create({ | ||
|
RayRedGoose marked this conversation as resolved.
|
||
| owner: 'Workday', | ||
| repo: 'canvas-tokens', | ||
| title: 'feat: Sync tokens from Figma', | ||
| head: '${{ steps.target-branch.outputs.name }}', | ||
| base: '${{ inputs.toBranch }}' | ||
| }); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,3 +39,5 @@ existing-tokens.json | |
|
|
||
| # Storybook | ||
| build-storybook.log | ||
|
|
||
| figma-raw-tokens/ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,15 @@ | ||
| export default { | ||
| testMatch: ['**/spec/**.spec.ts'], | ||
| transform: {'^.+\\.ts?$': 'ts-jest'}, | ||
| transform: { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to also modify the tsconfig to include the We'd just need to add: And change the Alternatively, if we want full typechecking & the ability to use Figma's types, we could convert the scripts to TS files and then run them using tsx instead of node directly. I have used it before & it works well. |
||
| '^.+\\.tsx?$': [ | ||
| 'ts-jest', | ||
| { | ||
| tsconfig: { | ||
| allowJs: true, | ||
| }, | ||
| }, | ||
| ], | ||
| }, | ||
| testEnvironment: 'node', | ||
| roots: ['packages/canvas-tokens', 'packages/canvas-tokens-web'], | ||
| testTimeout: 200000, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,13 +15,16 @@ | |
| "build:tokens": "nx build @workday/canvas-tokens", | ||
| "clean:tokens": "nx clean @workday/canvas-tokens-web", | ||
| "lint": "eslint -c ./.eslintrc.js --ext=ts .", | ||
| "fetch-tokens": "node scripts/utils/fetch-figma-variables.js", | ||
| "parse-tokens": "node scripts/utils/generate-dtcg-tokens.js", | ||
| "precommit": "lint-staged", | ||
| "prepare": "husky install", | ||
| "prestorybook": "yarn build:tokens", | ||
| "storybook": "nx storybook @workday/canvas-tokens-docs", | ||
| "build-storybook": "nx build-storybook @workday/canvas-tokens-docs", | ||
| "serve-storybook": "npx http-server docs/storybook/@workday/canvas-tokens-docs", | ||
| "test": "jest -c jest.config.ts", | ||
| "test": "jest -c jest.config.ts && vitest run -c vitest.config.ts", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Something for later: could add |
||
| "test:parser": "vitest run -c vitest.config.ts", | ||
| "typecheck": "tsc -p . --noEmit", | ||
| "prerelease": "yarn clean:tokens && yarn build:tokens", | ||
| "release": "changeset publish" | ||
|
|
@@ -58,7 +61,8 @@ | |
| "ts-jest": "^29.1.0", | ||
| "ts-node": "^10.9.2", | ||
| "typescript": "^5.0.4", | ||
| "vite": "~6.4.2" | ||
| "vite": "~6.4.2", | ||
| "vitest": "^3.0.5" | ||
| }, | ||
| "workspaces": [ | ||
| "packages/*" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,206 @@ | ||
| #!/usr/bin/env node | ||
| import fs from 'fs'; | ||
| import {resolve} from 'path'; | ||
|
|
||
|
RayRedGoose marked this conversation as resolved.
|
||
| const FIGMA_API_BASE = 'https://api.figma.com/v1'; | ||
| const DEFAULT_OUTPUT_DIR = 'figma-raw-tokens'; | ||
| const SKIP_STYLE_TYPES = ['FILL', 'GRID']; | ||
|
|
||
| const {FIGMA_ACCESS_TOKEN, FIGMA_BASE_FILE_KEY, FIGMA_MAIN_FILE_KEY} = process.env; | ||
|
|
||
| function warn(message) { | ||
| console.warn(`Warning: ${message}`); | ||
| } | ||
|
|
||
| /** | ||
| * Makes a request to the Figma API. | ||
| * @param {string} path - The path to the Figma API. | ||
| * @param {string} token - The Figma access token. | ||
| * @returns {Promise<Object>} The response from the Figma API. | ||
| */ | ||
| async function figmaRequest(path, token) { | ||
| try { | ||
| const response = await fetch(`${FIGMA_API_BASE}${path}`, { | ||
| headers: {'X-Figma-Token': token}, | ||
| }); | ||
|
|
||
| const body = await response.json(); | ||
|
|
||
| if (!response.ok || body.error) { | ||
| const message = | ||
| typeof body.message === 'string' | ||
| ? body.message | ||
| : typeof body.err === 'string' | ||
| ? body.err | ||
| : `Figma API request failed with status ${response.status}`; | ||
|
|
||
| throw new Error(message); | ||
| } | ||
|
|
||
| return body; | ||
| } catch (error) { | ||
| return {error: error.message}; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Fetches the variables from the Figma API. | ||
| * @param {string} fileKey - The key of the file to fetch. | ||
| * @param {string} token - The Figma access token. | ||
| * @returns {Promise<Object>} The variables from the Figma API. | ||
| */ | ||
| async function fetchVariables(fileKey, token) { | ||
| try { | ||
| const response = await figmaRequest(`/files/${fileKey}/variables/local`, token); | ||
|
|
||
| if (response.error || !response.meta) { | ||
| warn(`Variables fetch failed: ${response.error || 'No metadata returned'}`); | ||
| return {meta: {}}; | ||
| } | ||
|
|
||
| return {meta: response.meta}; | ||
| } catch (error) { | ||
| console.error(`Error fetching variables: ${error.message}`); | ||
| return {meta: {}}; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Fetches the published styles from the Figma API. | ||
| * @param {string} fileKey - The key of the file to fetch. | ||
| * @param {string} token - The Figma access token. | ||
| * @returns {Promise<Object>} The published styles from the Figma API. | ||
| */ | ||
| async function fetchPublishedStyles(fileKey, token) { | ||
| try { | ||
| const response = await figmaRequest(`/files/${fileKey}/styles`, token); | ||
|
|
||
| if (response.error || !Array.isArray(response.meta?.styles)) { | ||
| warn(`Published styles fetch failed: ${response.error || 'No styles metadata returned'}`); | ||
| return {styles: []}; | ||
| } | ||
|
|
||
| const styles = response.meta.styles.filter( | ||
| style => | ||
| !/more styles/i.test(style.name || '') && !SKIP_STYLE_TYPES.includes(style.style_type) | ||
| ); | ||
|
|
||
| return {styles}; | ||
| } catch (error) { | ||
| console.error(`Error fetching published styles: ${error.message}`); | ||
| return {styles: []}; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Fetches the style nodes from the Figma API. | ||
| * @param {string} fileKey - The key of the file to fetch. | ||
| * @param {string} token - The Figma access token. | ||
| * @param {Array} styles - The styles to fetch. | ||
| * @returns {Promise<Object>} The style nodes from the Figma API. | ||
| */ | ||
| async function fetchStyleNodes(fileKey, token, styles) { | ||
| try { | ||
| const nodeIds = [...new Set(styles.map(style => style.node_id).filter(Boolean))]; | ||
| const ids = encodeURIComponent(nodeIds.join(',')); | ||
| const response = nodeIds.length | ||
| ? await figmaRequest(`/files/${fileKey}/nodes?ids=${ids}`, token) | ||
| : {}; | ||
|
|
||
| if (response.error) { | ||
| warn(`Style nodes fetch failed: ${response.error}`); | ||
| return {nodes: {}, file: {}}; | ||
| } | ||
|
|
||
| const nodes = response.nodes || {}; | ||
|
|
||
| return { | ||
| file: { | ||
| name: response.name, | ||
| lastModified: response.lastModified, | ||
| version: response.version, | ||
| }, | ||
| nodes, | ||
| }; | ||
| } catch (error) { | ||
| console.error(`Error fetching style nodes: ${error.message}`); | ||
| return {nodes: {}, file: {}}; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Fetches the styles from the Figma API. | ||
| * @param {string} fileKey - The key of the file to fetch. | ||
| * @param {string} token - The Figma access token. | ||
| * @returns {Promise<Object>} The styles from the Figma API. | ||
| */ | ||
| async function fetchStyles(fileKey, token) { | ||
| try { | ||
| const published = await fetchPublishedStyles(fileKey, token); | ||
| const nodeResult = await fetchStyleNodes(fileKey, token, published.styles); | ||
|
|
||
| return { | ||
| published: published.styles, | ||
| nodes: nodeResult.nodes, | ||
| file: nodeResult.file, | ||
| }; | ||
| } catch (error) { | ||
| console.error(`Error fetching styles: ${error.message}`); | ||
| return {}; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Fetches the library from the Figma API. | ||
| * @param {string} fileKey - The key of the file to fetch. | ||
| * @param {string} token - The Figma access token. | ||
| * @returns {Promise<Object>} The library from the Figma API. | ||
| */ | ||
| async function fetchLibrary(fileKey, token) { | ||
| const variables = await fetchVariables(fileKey, token); | ||
| const styles = await fetchStyles(fileKey, token); | ||
| const name = fileKey === FIGMA_BASE_FILE_KEY ? 'Base' : 'Tokens'; | ||
|
|
||
| const published = styles.published || []; | ||
| const nodes = styles.nodes || {}; | ||
| const lastModified = styles.file?.lastModified || ''; | ||
|
|
||
| return { | ||
| library: { | ||
| name, | ||
| lastModified, | ||
| fetchedAt: new Date().toISOString(), | ||
| }, | ||
| meta: variables.meta, | ||
| styles: {published, nodes}, | ||
| }; | ||
| } | ||
|
|
||
| async function main() { | ||
| if (!FIGMA_ACCESS_TOKEN || !FIGMA_BASE_FILE_KEY || !FIGMA_MAIN_FILE_KEY) { | ||
| throw new Error( | ||
| 'Missing Figma access token or file keys. Set FIGMA_ACCESS_TOKEN, FIGMA_BASE_FILE_KEY, and FIGMA_MAIN_FILE_KEY.' | ||
| ); | ||
| } | ||
|
|
||
| const token = FIGMA_ACCESS_TOKEN || ''; | ||
| const fileKeys = [FIGMA_BASE_FILE_KEY, FIGMA_MAIN_FILE_KEY]; | ||
| const outputDirName = DEFAULT_OUTPUT_DIR; | ||
|
|
||
| const outputDir = resolve(process.cwd(), outputDirName); | ||
| fs.mkdirSync(outputDir, {recursive: true}); | ||
|
|
||
| for (const fileKey of fileKeys) { | ||
| console.log(`Fetching variables and styles for library file key: ${fileKey}`); | ||
|
|
||
| const payload = await fetchLibrary(fileKey, token); | ||
| const fileName = `${payload.library.name.toLowerCase()}.json`; | ||
| const outputPath = resolve(outputDir, fileName); | ||
| fs.writeFileSync(outputPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8'); | ||
| } | ||
| } | ||
|
|
||
| main().catch(error => { | ||
| console.error(`Error: ${error.message}`); | ||
| process.exitCode = 1; | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.