diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 530beef5..47bedfd0 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: - node-version: [20.x] + node-version: [20.10] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/update-tokens.yml b/.github/workflows/update-tokens.yml new file mode 100644 index 00000000..32a7521a --- /dev/null +++ b/.github/workflows/update-tokens.yml @@ -0,0 +1,57 @@ +name: Update Design Tokens + +on: + pull_request: + branches: + - figma + types: [opened, synchronize, reopened] + +jobs: + update-tokens: + runs-on: ubuntu-latest + + permissions: + contents: write + pull-requests: write + + strategy: + matrix: + node-version: [20.10] + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - run: npm ci + + - name: Run token updater + run: npm run tokens:update + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Run build tokens + run: npm run build:tokens + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Check for changes + id: check-changes + run: | + if [[ -n "$(git status --porcelain)" ]]; then + echo "hasChanges=true" >> $GITHUB_OUTPUT + else + echo "hasChanges=false" >> $GITHUB_OUTPUT + fi + + - name: Commit changes + if: steps.check-changes.outputs.hasChanges == 'true' + run: | + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add . + git commit -m "chore: update design tokens from Figma [skip ci]" + git push diff --git a/package.json b/package.json index b7c2fa4b..7babed64 100644 --- a/package.json +++ b/package.json @@ -42,8 +42,9 @@ "build:grid": "node grid.mjs", "site-tokens": "node ./site-tokens.mjs", "validate": "node ./validate.mjs", - "format": "eslint --fix \"style-dictionary/**/*.mjs\"", - "lint": "eslint \"style-dictionary/**/*.mjs\"" + "format": "eslint --fix \"{style-dictionary,tokens-studio}/**/*.mjs\"", + "lint": "eslint \"style-dictionary/**/*.mjs\"", + "tokens:update": "node ./tokens-studio/token-updater.mjs" }, "devDependencies": { "@divriots/style-dictionary-to-figma": "^0.4.0", diff --git a/tokens-studio/token-updater.mjs b/tokens-studio/token-updater.mjs new file mode 100644 index 00000000..d7995aed --- /dev/null +++ b/tokens-studio/token-updater.mjs @@ -0,0 +1,105 @@ +import fs from 'fs-extra' +import path from 'path' +import { getDirname } from '../style-dictionary/utils.mjs' + +const __dirname = getDirname(import.meta.url) +const FIGMA_TOKENS_PATH = path.resolve(__dirname, '../dist/rei-dot-com/figma/figma.json') + +function flattenTokens (obj, parentPath = [], result = {}, filePathMap = {}) { + for (const [key, value] of Object.entries(obj)) { + const currentPath = [...parentPath, key] + + if (value.$value !== undefined && value.$type !== undefined) { + const tokenPath = currentPath.join('.') + result[tokenPath] = { + ...value, // Keep all properties + filePath: value.filePath + } + + if (value.filePath) { + const filePath = value.filePath + if (!filePathMap[filePath]) { + filePathMap[filePath] = {} + } + filePathMap[filePath][tokenPath] = result[tokenPath] + } + } else if (typeof value === 'object' && value !== null) { + flattenTokens(value, currentPath, result, filePathMap) + } + } + + return { flatTokens: result, filePathMap } +} + +async function updateTokensInPlace (obj, sourceTokens, parentPath = []) { + for (const [key, value] of Object.entries(obj)) { + const currentPath = [...parentPath, key] + const tokenPath = currentPath.join('.') + + if (value.$value !== undefined) { + // If this path exists in source tokens, update only the $value + const sourceToken = sourceTokens[tokenPath] + if (sourceToken?.$value !== undefined) { + value.$value = sourceToken.$value + } + } else if (typeof value === 'object' && value !== null) { + await updateTokensInPlace(value, sourceTokens, currentPath) + } + } +} + +async function updateTokens (targetFilePath) { + try { + // Read both source and target files + const targetContent = await fs.readJson(targetFilePath) + const sourceContent = await fs.readJson(FIGMA_TOKENS_PATH) + + // Flatten source tokens + const { flatTokens: sourceFlatTokens } = flattenTokens(sourceContent) + + // Update the target content in place + await updateTokensInPlace(targetContent, sourceFlatTokens) + + // Write the updated content back to the file + await fs.writeJson(targetFilePath, targetContent, { spaces: 2 }) + + return { + file: targetFilePath, + updated: true + } + } catch (error) { + console.error(`Error updating tokens in ${targetFilePath}:`, error) + throw error + } +} + +async function getUniqueFilePaths () { + try { + const content = await fs.readJson(FIGMA_TOKENS_PATH) + const { filePathMap } = flattenTokens(content) + return Object.keys(filePathMap) + } catch (error) { + console.error('Error getting unique files:', error) + throw error + } +} + +async function main () { + try { + const files = await getUniqueFilePaths() + const results = [] + + for (const file of files.slice(0, 1)) { + const update = await updateTokens(file) + results.push(update) + console.log(`Updated ${file}`) + } + + return results + } catch (error) { + console.error('Error in main:', error) + throw error + } +} + +main()